diff --git a/web-ui/src/helpers/colors.js b/web-ui/src/helpers/colors.js new file mode 100644 index 0000000000..e2bf178af3 --- /dev/null +++ b/web-ui/src/helpers/colors.js @@ -0,0 +1,31 @@ +// pSBC - Shade Blend Convert - Version 4.1 - 01/7/2021 +// https://github.com/PimpTrizkit/PJs/blob/master/pSBC.js + +export const pSBC=(p,c0,c1,l)=>{ + let r,g,b,P,f,t,h,m=Math.round,a=typeof(c1)=="string"; + if(typeof(p)!="number"||p<-1||p>1||typeof(c0)!="string"||(c0[0]!='r'&&c0[0]!='#')||(c1&&!a))return null; + h=c0.length>9,h=a?c1.length>9?true:c1=="c"?!h:false:h,f=pSBC.pSBCr(c0),P=p<0,t=c1&&c1!="c"?pSBC.pSBCr(c1):P?{r:0,g:0,b:0,a:-1}:{r:255,g:255,b:255,a:-1},p=P?p*-1:p,P=1-p; + if(!f||!t)return null; + if(l)r=m(P*f.r+p*t.r),g=m(P*f.g+p*t.g),b=m(P*f.b+p*t.b); + else r=m((P*f.r**2+p*t.r**2)**0.5),g=m((P*f.g**2+p*t.g**2)**0.5),b=m((P*f.b**2+p*t.b**2)**0.5); + a=f.a,t=t.a,f=a>=0||t>=0,a=f?a<0?t:t<0?a:a*P+t*p:0; + if(h)return"rgb"+(f?"a(":"(")+r+","+g+","+b+(f?","+m(a*1000)/1000:"")+")"; + else return"#"+(4294967296+r*16777216+g*65536+b*256+(f?m(a*255):0)).toString(16).slice(1,f?undefined:-2) +} + +pSBC.pSBCr=(d)=>{ + const i=parseInt; + let n=d.length,x={}; + if(n>9){ + const [r, g, b, a] = (d = d.split(',')); + n = d.length; + if(n<3||n>4)return null; + x.r=i(r[3]=="a"?r.slice(5):r.slice(4)),x.g=i(g),x.b=i(b),x.a=a?parseFloat(a):-1 + }else{ + if(n==8||n==6||n<4)return null; + if(n<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(n>4?d[4]+d[4]:""); + d=i(d.slice(1),16); + if(n==9||n==5)x.r=d>>24&255,x.g=d>>16&255,x.b=d>>8&255,x.a=Math.round((d&255)/0.255)/1000; + else x.r=d>>16,x.g=d>>8&255,x.b=d&255,x.a=-1 + }return x +}; diff --git a/web-ui/src/helpers/index.js b/web-ui/src/helpers/index.js index 0cebfe6af0..bf72c325e5 100644 --- a/web-ui/src/helpers/index.js +++ b/web-ui/src/helpers/index.js @@ -4,3 +4,4 @@ export * from './datetime'; export * from './query-parameters'; export * from './sanitizehtml'; export * from './strings'; +export * from './colors'; diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 24926e5a4d..4cf902f10a 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -6,6 +6,9 @@ import { BarChart, CartesianGrid, Legend, + Pie, + Cell, + PieChart, ResponsiveContainer, Tooltip, XAxis, @@ -32,11 +35,13 @@ import { TextField, Typography } from '@mui/material'; - +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { pSBC } from '../helpers/colors.js'; import { getAvatarURL, resolve } from '../api/api.js'; import MemberSelector from '../components/member_selector/MemberSelector'; import { AppContext } from '../context/AppContext.jsx'; @@ -54,8 +59,7 @@ import './PulseReportPage.css'; // easily use color variables defined in variables.css. const ociDarkBlue = '#2c519e'; //const ociLightBlue = '#76c8d4'; // not currently used -// const ociOrange = '#f8b576'; // too light -const orange = '#b26801'; +const ociOrange = '#f8b576'; const ScoreOption = { INTERNAL: 'Internal', @@ -69,6 +73,12 @@ const propertyMap = { [ScoreOption.COMBINED]: 'combinedAverage' }; +const ScoreOptionLabel = { + 'Internal': 'At Work', + 'External': 'Outside Work', + 'Combined': 'Both', +}; + /* // Returns a random, integer score between 1 and 5. // We may want to uncomment this later for testing. @@ -112,6 +122,8 @@ const PulseReportPage = () => { const [selectedPulse, setSelectedPulse] = useState(null); const [showComments, setShowComments] = useState(false); const [teamMembers, setTeamMembers] = useState([]); + const [internalPieChartData, setInternalPieChartData] = useState([]); + const [externalPieChartData, setExternalPieChartData] = useState([]); /* // This generates random data to use in the line chart. @@ -213,6 +225,38 @@ const PulseReportPage = () => { } } + let internalPieCounts = [ + {name: "internalVeryDissatisfied", value: 0}, + {name: "internalDissatisfied", value: 0}, + {name: "internalNeutral", value: 0}, + {name: "internalSatisfied", value: 0}, + {name: "internalVerySatisfied", value: 0}, + ]; + for(let day of scoreChartDataPoints) { + day.datapoints.forEach(datapoint => { + internalPieCounts[datapoint.internalScore - 1].value++; + }); + } + // Filter out data with a zero value so that the pie chart does not attempt + // to display them. + setInternalPieChartData(internalPieCounts.filter((p) => p.value != 0)); + + let externalPieCounts = [ + {name: "externalVeryDissatisfied", value: 0}, + {name: "externalDissatisfied", value: 0}, + {name: "externalNeutral", value: 0}, + {name: "externalSatisfied", value: 0}, + {name: "externalVerySatisfied", value: 0}, + ]; + for(let day of scoreChartDataPoints) { + day.datapoints.forEach(datapoint => { + externalPieCounts[datapoint.externalScore - 1].value++; + }); + } + // Filter out data with a zero value so that the pie chart does not attempt + // to display them. + setExternalPieChartData(externalPieCounts.filter((p) => p.value != 0)); + setScoreChartData(scoreChartDataPoints.map(day => { const iScores = {}; const eScores = {}; @@ -322,27 +366,6 @@ const PulseReportPage = () => {
Average Scores - - setScoreType(e.target.value)} - sx={{ width: '8rem' }} - value={scoreType} - variant="outlined" - > - - {ScoreOption.INTERNAL} - - - {ScoreOption.EXTERNAL} - - - {ScoreOption.COMBINED} - - - { ); - const barChart = () => ( + const scoreDistributionChart = () => ( { - - + {(scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.INTERNAL) && + + } + {(scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.EXTERNAL) && + + } { }; const dataInfo = [ - {key: "internalVeryDissatisfied", stackId: "internal", color: "#273e58", }, - {key: "internalDissatisfied", stackId: "internal", color: "#1a3c6d", }, - {key: "internalNeutral", stackId: "internal", color: "#2c519e", }, - {key: "internalSatisfied", stackId: "internal", color: "#4b7ac7", }, - {key: "internalVerySatisfied", stackId: "internal", color: "#6fa3e6", }, - {key: "externalVeryDissatisfied", stackId: "external", color: "#704401", }, - {key: "externalDissatisfied", stackId: "external", color: "#8a5200", }, - {key: "externalNeutral", stackId: "external", color: "#b26801", }, - {key: "externalSatisfied", stackId: "external", color: "#d48a2c", }, - {key: "externalVerySatisfied", stackId: "external", color: "#e0a456", }, + {key: "internalVeryDissatisfied", stackId: "internal", color: pSBC(-.9, ociDarkBlue), }, + {key: "internalDissatisfied", stackId: "internal", color: pSBC(-.75, ociDarkBlue), }, + {key: "internalNeutral", stackId: "internal", color: pSBC(-.5, ociDarkBlue), }, + {key: "internalSatisfied", stackId: "internal", color: pSBC(-.25, ociDarkBlue), }, + {key: "internalVerySatisfied", stackId: "internal", color: ociDarkBlue, }, + {key: "externalVeryDissatisfied", stackId: "external", color: pSBC(-.9, ociOrange), }, + {key: "externalDissatisfied", stackId: "external", color: pSBC(-.75, ociOrange), }, + {key: "externalNeutral", stackId: "external", color: pSBC(-.5, ociOrange), }, + {key: "externalSatisfied", stackId: "external", color: pSBC(-.25, ociOrange), }, + {key: "externalVerySatisfied", stackId: "external", color: ociOrange, }, ]; const labelToSentiment = (label) => { - const suffix = label.includes("internal") ? "At Work" : "Outside Work"; + const suffix = label.includes("internal") + ? ScoreOptionLabel[ScoreOption.INTERNAL] + : ScoreOptionLabel[ScoreOption.EXTERNAL]; switch(label.replace("internal", "").replace("external", "")) { case "VeryDissatisfied": return <> {suffix}; @@ -468,7 +505,7 @@ const PulseReportPage = () => {

{label}

{payload.slice().reverse().map(p => { - return
+ return
{p.value} {p.name.props.children}
; })} @@ -501,10 +538,99 @@ const PulseReportPage = () => { ); }; - const lineChart = () => ( + const sectionTitle = (prefix) => { + let title = `${prefix} for`; + if (scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.INTERNAL) { + title += ` "${ScoreOptionLabel[ScoreOption.INTERNAL]}"`; + } + if (scoreType == ScoreOption.COMBINED) { + title += " and"; + } + if (scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.EXTERNAL) { + title += ` "${ScoreOptionLabel[ScoreOption.EXTERNAL]}"`; + } + return title; + }; + + const pieLabelToSentiment = (label) => { + switch(label.replace("internal", "").replace("external", "")) { + case "VeryDissatisfied": + return "😦"; + case "Dissatisfied": + return "🙁"; + case "Neutral": + return "😐"; + case "Satisfied": + return "🙂"; + case "VerySatisfied": + return "😀"; + } + return "ERROR"; + }; + + const RADIAN = Math.PI / 180; + const renderPieLabel = function({ cx, cy, midAngle, innerRadius, outerRadius, + percent, index, name, value }) { + const radius = innerRadius + (outerRadius - innerRadius) * 0.5; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + + return ( + <> + cx ? 'start' : 'end'} + dominantBaseline="central"> + {pieLabelToSentiment(name)} {value} + + + ); + }; + + const titleWords = (text) => { + if (text.match(/^[a-z]+$/)) { + // Uppercase the first letter + text = text[0].toUpperCase() + text.substring(1); + } else { + // Split words and uppercase the first word + let words = text.split(/(?<=[a-z])(?=[A-Z\d])/); + words[0] = words[0][0].toUpperCase() + words[0].substring(1); + text= ""; + let separator = ""; + for(let word of words) { + text += `${separator}${word}`; + separator = " "; + } + } + return text; + }; + + const CustomPieTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

+ {titleWords(payload[0].name + .replace("internal", "") + .replace("external", ""))} : {payload[0].value}

+
+ ); + } + + return null; + }; + + const pieSliceColor = (entry, index) => { + return value.key == entry.name).color} />; + }; + + const pulseScoresChart = () => ( + <> @@ -522,7 +648,12 @@ const PulseReportPage = () => { - {dataInfo.map((obj) => { + {dataInfo.filter(o => scoreType == ScoreOption.COMBINED || + (scoreType == ScoreOption.INTERNAL && + o.key.includes("internal")) || + (scoreType == ScoreOption.EXTERNAL && + o.key.includes("external"))) + .map((obj) => { return { + + + +
+ {(scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.INTERNAL) && +
+ + + } + /> + + {internalPieChartData.map(pieSliceColor)} + + + +
+ } + {(scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.EXTERNAL) && +
+ + + } + /> + + {externalPieChartData.map(pieSliceColor)} + + + +
+ } +
+
+
+ ); const responseSummary = () => { @@ -587,6 +777,25 @@ const PulseReportPage = () => { ); }; + const toggleLabels = { + left: { + title: ScoreOptionLabel[ScoreOption.INTERNAL], + value: ScoreOption.INTERNAL, + }, + center: { + title: ScoreOptionLabel[ScoreOption.Combined], + value: ScoreOption.COMBINED, + }, + right: { + title: ScoreOptionLabel[ScoreOption.EXTERNAL], + value: ScoreOption.EXTERNAL, + }, + }; + + const toggleChange = (event, value) => { + setScoreType(value); + }; + return selectHasViewPulseReportPermission(state) ? (
@@ -604,6 +813,18 @@ const PulseReportPage = () => { value={dayjs(dateTo)} /> + + + {ScoreOptionLabel[ScoreOption.INTERNAL]} + + {ScoreOptionLabel[ScoreOption.COMBINED]} + + {ScoreOptionLabel[ScoreOption.EXTERNAL]} +
{pulses.length === 0 ? ( @@ -617,9 +838,9 @@ const PulseReportPage = () => { onChange={handleTeamMembersChange} selected={teamMembers} /> - {lineChart()} + {pulseScoresChart()} {averageScores()} - {barChart()} + {scoreDistributionChart()} setShowComments(false)}>
+
+ + + +