diff --git a/src/components/TotalOrgSummary/TotalOrgSummary.jsx b/src/components/TotalOrgSummary/TotalOrgSummary.jsx index d7be79d5a5..7e542b893c 100644 --- a/src/components/TotalOrgSummary/TotalOrgSummary.jsx +++ b/src/components/TotalOrgSummary/TotalOrgSummary.jsx @@ -177,7 +177,9 @@ function TotalOrgSummary(props) { await new Promise(resolve => setTimeout(resolve, 5000)); // 3. Replace Chart.js canvas elements with images in the live DOM. - const chartCanvases = document.querySelectorAll('.volunteer-status-chart canvas'); + const chartCanvases = document.querySelectorAll( + '[data-chart="volunteer-status"] canvas, [data-chart="mentor-status"] canvas', + ); const originalCanvases = []; chartCanvases.forEach(canvasElem => { try { @@ -673,7 +675,11 @@ ${
diff --git a/src/components/TotalOrgSummary/TotalOrgSummary.module.css b/src/components/TotalOrgSummary/TotalOrgSummary.module.css index c19a368002..eb9443fdd4 100644 --- a/src/components/TotalOrgSummary/TotalOrgSummary.module.css +++ b/src/components/TotalOrgSummary/TotalOrgSummary.module.css @@ -1,15 +1,10 @@ +/* Chart labels stay dark in dark mode for readability against light label chips */ .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer .recharts-wrapper text, .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer .recharts-wrapper tspan { fill: #000 !important; color: #000 !important; text-shadow: 1px 1px 3px rgba(0,0,0,0.25), 0 0 2px #fff; } -/* Chart title stays white, but chart numbers/labels inside the donut graph are black for better contrast */ -.containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer .recharts-wrapper text, -.containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer .recharts-wrapper tspan { - fill: #000 !important; - color: #000 !important; -} /* Chart and graph titles/text should be white in dark mode for visibility */ .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer h3, .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer p, @@ -73,11 +68,6 @@ box-shadow: 0 4px 12px rgba(0, 123, 255, 0.4) !important; } -.containerTotalOrgWrapper:global(.bg-oxford-blue) .componentBorder { - background-color: #1c2541 !important; - border: none !important; -} - .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer { background-color: #1c2541 !important; border: none !important; @@ -279,25 +269,6 @@ } /* Dark mode dropdown consistency */ -.containerTotalOrgWrapper:global(.bg-oxford-blue) .reportHeaderActions :global(.dropdown-toggle) { - background-color: #6f42c1 !important; - border-color: #6f42c1 !important; -} - -.containerTotalOrgWrapper:global(.bg-oxford-blue) .reportHeaderActions :global(.dropdown-menu) { - background-color: #1c2541 !important; - border-color: #6f42c1 !important; -} - -.containerTotalOrgWrapper:global(.bg-oxford-blue) .reportHeaderActions :global(.dropdown-item) { - background-color: #1c2541 !important; - color: #fff !important; -} - -.containerTotalOrgWrapper:global(.bg-oxford-blue) .reportHeaderActions :global(.dropdown-item):hover { - background-color: #6f42c1 !important; -} - /* Component containers - Clean borderless design */ .componentContainer { margin: 15px 0; @@ -315,6 +286,10 @@ background-color: #fff; overflow: hidden; } + +.componentBorderLoose { + overflow: visible; +} .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentBorder { background-color: #1c2541 !important; border: 1.5px solid #2f4157 !important; diff --git a/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.jsx b/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.jsx new file mode 100644 index 0000000000..f4d36a9a61 --- /dev/null +++ b/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.jsx @@ -0,0 +1,109 @@ +import PropTypes from 'prop-types'; +import { Doughnut } from 'react-chartjs-2'; +import { Chart, ArcElement } from 'chart.js'; +import styles from './MentorStatusPieChart.module.css'; +import externalLabelGuidesPlugin from './externalLabelGuidesPlugin'; + +Chart.register(ArcElement); + +function MentorStatusPieChart({ + data: { totalMentors, percentageChange, data: mentorData }, + comparisonType, +}) { + const chartData = { + labels: mentorData.map(item => item.label), + datasets: [ + { + data: mentorData.map(item => item.value), + backgroundColor: ['#287D5A', '#2D9DA6', '#F26B38'], + borderWidth: 1, + }, + ], + }; + + const options = { + plugins: { + datalabels: { + display: false, + }, + legend: { + display: false, + }, + tooltip: { + enabled: false, + }, + externalLabelGuides: { + offset: 20, + horizontalSpread: 32, + horizontalSpreadMap: { 0: 32, 1: 46, 2: 5 }, + verticalOffsetMap: { 0: 34, 1: -20, 2: -46 }, + sideMap: { 0: 1, 1: -1, 2: 1 }, + total: totalMentors, + formatter: ({ value, percentage }) => [`${value}`, `(${percentage}%)`], + }, + }, + maintainAspectRatio: false, + cutout: '60%', + layout: { + padding: 20, + }, + }; + + const percentageChangeColor = percentageChange >= 0 ? 'green' : 'red'; + + return ( +
+
+ +
+

TOTAL MENTORS

+

{totalMentors}

+ {comparisonType !== 'No Comparison' && ( +

+ {percentageChange >= 0 + ? `+${percentageChange}% ${comparisonType.toUpperCase()}` + : `${percentageChange}% ${comparisonType.toUpperCase()}`} +

+ )} +
+
+
+ {mentorData.map((item, index) => ( +
+
+ ))} +
+
+ ); +} + +MentorStatusPieChart.propTypes = { + data: PropTypes.shape({ + totalMentors: PropTypes.number.isRequired, + percentageChange: PropTypes.number.isRequired, + data: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.number.isRequired, + }), + ).isRequired, + }).isRequired, + comparisonType: PropTypes.string.isRequired, +}; + +export default MentorStatusPieChart; diff --git a/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.module.css b/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.module.css new file mode 100644 index 0000000000..abf0772c15 --- /dev/null +++ b/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.module.css @@ -0,0 +1,106 @@ +.mentorStatusContainer { + margin-top: 24px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + width: 100%; + height: 100%; + gap: 12px; +} + +.mentorStatusChart { + position: relative; + width: min(320px, 100%); + max-width: 320px; + aspect-ratio: 1 / 1; + overflow: visible; +} + +.mentorStatusCenter { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + font-size: 14px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.mentorStatusHeading { + color: #828282; + font-size: 1.2rem; + letter-spacing: 0.5px; + text-transform: uppercase; + transform: translateY(6px); + width: 100%; + text-align: center; +} + +.mentorCount { + color: #6c6c6c; + font-size: 1.8rem; + font-weight: 800; + line-height: 1.18; + margin-top: -10px; +} + +.mentorPercentageChange { + font-weight: 600; +} + +.mentorStatusLabels { + display: flex; + justify-content: center; + align-items: center; + gap: 16px; + margin-top: 48px; + margin-bottom: 24px; + flex-wrap: nowrap; + padding: 0 20px; + width: max-content; + max-width: 100%; + box-sizing: border-box; +} + +.mentorStatusLabel { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.875rem; + white-space: nowrap; +} + +.mentorStatusColor { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; +} + +:global(.bg-oxford-blue) .mentorStatusHeading { + color: #f1f5ff; +} + +:global(.bg-oxford-blue) .mentorCount { + color: #ffffff; +} + +@media (max-width: 768px) { + .mentorStatusChart { + width: min(280px, 100%); + } + + .mentorStatusLabels { + gap: 12px; + } +} + +@media (min-width: 768px) { + .mentorStatusChart { + max-width: 280px; + } +} diff --git a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.jsx b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.jsx index ed885a04c8..7748f8e143 100644 --- a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.jsx +++ b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.jsx @@ -2,9 +2,16 @@ import { useMemo } from 'react'; import PropTypes from 'prop-types'; import Loading from '~/components/common/Loading'; import VolunteerStatusPieChart from './VolunteerStatusPieChart'; +import MentorStatusPieChart from './MentorStatusPieChart'; +import styles from './VolunteerStatusChart.module.css'; -function VolunteerStatusChart({ isLoading, volunteerNumberStats, comparisonType }) { - const chartData = useMemo(() => { +function VolunteerStatusChart({ + isLoading, + volunteerNumberStats, + mentorNumberStats, + comparisonType, +}) { + const volunteerChartData = useMemo(() => { if (!volunteerNumberStats) { return null; } @@ -42,8 +49,43 @@ function VolunteerStatusChart({ isLoading, volunteerNumberStats, comparisonType }; }, [volunteerNumberStats]); + const mentorChartData = useMemo(() => { + if (!mentorNumberStats) { + return null; + } + + const { + donutChartData, + activeMentors, + deactivatedMentors, + newMentors, + totalMentors, + } = mentorNumberStats; + + let chartDataValues; + if (donutChartData && donutChartData.existingActive !== undefined) { + chartDataValues = [ + { label: 'Existing Active', value: donutChartData.existingActive.count }, + { label: 'New Active', value: donutChartData.newActive.count }, + { label: 'Deactivated', value: donutChartData.deactivated.count }, + ]; + } else { + chartDataValues = [ + { label: 'Active', value: activeMentors.count }, + { label: 'New', value: newMentors.count }, + { label: 'Deactivated This Week', value: deactivatedMentors.count }, + ]; + } + + return { + totalMentors: totalMentors.count, + percentageChange: Number(totalMentors.comparisonPercentage) || 0, + data: chartDataValues, + }; + }, [mentorNumberStats]); + return ( -
+
{isLoading ? (
@@ -51,7 +93,28 @@ function VolunteerStatusChart({ isLoading, volunteerNumberStats, comparisonType
) : ( - + <> +
+
+ {volunteerChartData && ( + + )} +
+ {mentorChartData && ( +
+ +
+ )} +
+ {(volunteerChartData || mentorChartData) && ( +

+ *Does not include the “Mentor” members shown in the graph to the right. +

+ )} + )}
); @@ -86,6 +149,32 @@ VolunteerStatusChart.propTypes = { comparisonPercentage: PropTypes.number, }), }), + mentorNumberStats: PropTypes.shape({ + donutChartData: PropTypes.shape({ + existingActive: PropTypes.shape({ + count: PropTypes.number, + }), + newActive: PropTypes.shape({ + count: PropTypes.number, + }), + deactivated: PropTypes.shape({ + count: PropTypes.number, + }), + }), + activeMentors: PropTypes.shape({ + count: PropTypes.number, + }), + newMentors: PropTypes.shape({ + count: PropTypes.number, + }), + deactivatedMentors: PropTypes.shape({ + count: PropTypes.number, + }), + totalMentors: PropTypes.shape({ + count: PropTypes.number, + comparisonPercentage: PropTypes.number, + }), + }), }; export default VolunteerStatusChart; diff --git a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.module.css b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.module.css new file mode 100644 index 0000000000..4968093f95 --- /dev/null +++ b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.module.css @@ -0,0 +1,69 @@ +.chartRoot { + margin-top: 1.5rem; + height: 100%; + display: flex; + flex-direction: column; +} + +.volunteerMentorChartsWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 24px; + width: 100%; + flex: 1 1 auto; +} + +.volunteerChartSection, +.mentorChartSection { + width: 100%; + display: flex; + justify-content: center; + align-items: stretch; + flex: 1 1 100%; + min-width: 0; +} + +@media (min-width: 768px) { + .volunteerMentorChartsWrapper { + flex-direction: row; + flex-wrap: wrap; + align-items: stretch; + justify-content: center; + } + + .volunteerChartSection, + .mentorChartSection { + flex: 1 1 280px; + max-width: 360px; + } + + .chartRoot { + min-height: 520px; + } +} + +@media (min-width: 1200px) { + .volunteerMentorChartsWrapper { + flex-wrap: nowrap; + justify-content: space-evenly; + } + + .volunteerChartSection { + flex: 1 1 340px; + } + + .mentorChartSection { + flex: 0 1 320px; + } +} + +.volunteerMentorFootnote { + margin-top: 105px; + text-align: center; + font-size: 0.75rem; + color: #4f4f4f; + max-width: 560px; + align-self: center; +} diff --git a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.css b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.css deleted file mode 100644 index da4fed0d08..0000000000 --- a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.css +++ /dev/null @@ -1,62 +0,0 @@ -.volunteer-status-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; -} - -.volunteer-status-chart { - position: relative; - width: 100%; - max-width: 400px; - height: 400px; -} - -.volunteer-status-center { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; - font-size: 14px; -} - -.volunteer-status-center .volunteer-status-heading { - color: #828282; - font-size: 1.2rem; -} - -.volunteer-status-center .volunteer-count { - color: #6c6c6c; - font-size: 2rem; - font-weight: bolder; -} - -.volunteer-status-center > p { - font-weight: bold; -} - -.volunteer-status-center div { - margin: 2px 0; -} - -.volunteer-status-labels { - display: flex; - justify-content: center; - margin-top: 20px; - margin-bottom: 50px; -} - -.volunteer-status-label { - display: flex; - align-items: center; - margin: 0 10px; -} - -.volunteer-status-color { - display: inline-block; - width: 12px; - height: 12px; - margin-right: 5px; -} diff --git a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.jsx b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.jsx index bf3f82c9d8..54c8f10286 100644 --- a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.jsx +++ b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.jsx @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import { Doughnut } from 'react-chartjs-2'; -import ChartDataLabels from 'chartjs-plugin-datalabels'; import { Chart, ArcElement } from 'chart.js'; -import './VolunteerStatusPieChart.css'; +import styles from './VolunteerStatusPieChart.module.css'; +import externalLabelGuidesPlugin from './externalLabelGuidesPlugin'; Chart.register(ArcElement); @@ -26,21 +26,8 @@ function VolunteerStatusPieChart({ const options = { plugins: { datalabels: { - color: '#000', - font: { - size: 20, - weight: 'bolder', - lineHeight: 1.8, - }, - formatter: function(value, context) { - const percentage = ((value / totalVolunteers) * 100).toFixed(0); - // Show value and percent as two lines for clarity - return [`${value}`, `(${percentage}%)`]; - }, - display: true, - offset: 0, - align: 'center', - anchor: 'center', + // Hide in-slice labels because values are already shown with external guides. + display: false, }, legend: { display: false, @@ -48,23 +35,39 @@ function VolunteerStatusPieChart({ tooltip: { enabled: false, }, + externalLabelGuides: { + offset: 20, + horizontalSpread: 34, + horizontalSpreadMap: { 0: 34, 1: 48, 2: 5 }, + verticalOffsetMap: { 0: 38, 1: -22, 2: -50 }, + sideMap: { 0: 1, 1: -1, 2: 1 }, + total: totalVolunteers, + formatter: ({ value, percentage }) => [`${value}`, `(${percentage}%)`], + }, }, maintainAspectRatio: false, cutout: '55%', + layout: { + padding: 24, + }, }; const percentageChangeColor = percentageChange >= 0 ? 'green' : 'red'; return ( -
-
- -
-

TOTAL VOLUNTEERS

-

{totalVolunteers}

+
+
+ +
+

TOTAL VOLUNTEERS*

+

{totalVolunteers}

{comparisonType !== 'No Comparison' && (

@@ -75,11 +78,11 @@ function VolunteerStatusPieChart({ )}

-
+
{volunteerData.map((item, index) => ( -
+