From 6f64ea631da52476cae3aef9234c278dbd77e5fa Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 19 Dec 2024 08:58:13 -0600 Subject: [PATCH 1/6] Added a toggle for at-work/outside-work/both and a pie chart showing breakdown of responses (with emojis). --- web-ui/src/pages/PulseReportPage.jsx | 230 +++++++++++++++++++++++---- 1 file changed, 197 insertions(+), 33 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 385db37dd..4e1a5badd 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -6,6 +6,8 @@ import { BarChart, CartesianGrid, Legend, + Pie, + PieChart, ResponsiveContainer, Tooltip, XAxis, @@ -32,7 +34,8 @@ 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'; @@ -53,7 +56,7 @@ import './PulseReportPage.css'; // Recharts doesn't support using CSS variables, so we can't // easily use color variables defined in variables.css. const ociDarkBlue = '#2c519e'; -//const ociLightBlue = '#76c8d4'; // not currently used +const ociLightBlue = '#76c8d4'; // const ociOrange = '#f8b576'; // too light const orange = '#b26801'; @@ -69,6 +72,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 +121,7 @@ const PulseReportPage = () => { const [selectedPulse, setSelectedPulse] = useState(null); const [showComments, setShowComments] = useState(false); const [teamMembers, setTeamMembers] = useState([]); + const [pieChartData, setPieChartData] = useState([]); /* // This generates random data to use in the line chart. @@ -210,6 +220,21 @@ const PulseReportPage = () => { } } + let pieCounts = [ + {name: "veryDissatisfied", value: 0}, + {name: "dissatisfied", value: 0}, + {name: "neutral", value: 0}, + {name: "satisfied", value: 0}, + {name: "verySatisfied", value: 0}, + ]; + for(let day of scoreChartDataPoints) { + day.datapoints.forEach(datapoint => { + pieCounts[datapoint.internalScore - 1].value++; + pieCounts[datapoint.externalScore - 1].value++; + }); + } + setPieChartData(pieCounts); + setScoreChartData(scoreChartDataPoints.map(day => { const iScores = {}; const eScores = {}; @@ -319,27 +344,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 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}; @@ -465,7 +483,7 @@ const PulseReportPage = () => {

{label}

{payload.map(p => { - return
+ return
{p.value} {p.name.props.children}
; })} @@ -498,10 +516,95 @@ const PulseReportPage = () => { ); }; - const lineChart = () => ( + const pulseScoresTitle = () => { + let title = "Pulse scores 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.toLowerCase()) { + case "verydissatisfied": + //return ; + return "😦"; + case "dissatisfied": + //return ; + return "🙁"; + case "neutral": + //return ; + return "😐"; + case "satisfied": + //return ; + return "🙂"; + case "verysatisfied": + //return ; + 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)} : {payload[0].value}

+
+ ); + } + + return null; + }; + + const pulseScoresChart = () => ( + <> @@ -519,7 +622,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 { + + + + + + } + /> + + + + + + ); const responseSummary = () => { @@ -584,6 +717,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) ? (
@@ -601,6 +753,18 @@ const PulseReportPage = () => { value={dayjs(dateTo)} /> + + + {ScoreOptionLabel[ScoreOption.INTERNAL]} + + {ScoreOptionLabel[ScoreOption.COMBINED]} + + {ScoreOptionLabel[ScoreOption.EXTERNAL]} +
{pulses.length === 0 ? ( @@ -614,9 +778,9 @@ const PulseReportPage = () => { onChange={handleTeamMembersChange} selected={teamMembers} /> - {lineChart()} + {pulseScoresChart()} {averageScores()} - {barChart()} + {scoreDistributionChart()} setShowComments(false)}> Date: Thu, 19 Dec 2024 09:12:24 -0600 Subject: [PATCH 2/6] Removed commented sentiment icons. --- web-ui/src/pages/PulseReportPage.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 4e1a5badd..3ad40a8eb 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -535,19 +535,14 @@ const PulseReportPage = () => { const pieLabelToSentiment = (label) => { switch(label.toLowerCase()) { case "verydissatisfied": - //return ; return "😦"; case "dissatisfied": - //return ; return "🙁"; case "neutral": - //return ; return "😐"; case "satisfied": - //return ; return "🙂"; case "verysatisfied": - //return ; return "😀"; } return "ERROR"; From a0111da4ce91c336696a88559f311e7d74130aa0 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 19 Dec 2024 09:24:23 -0600 Subject: [PATCH 3/6] Updated to reflect the removal of the drop-down. --- .../PulseReportPage.test.jsx.snap | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/web-ui/src/pages/__snapshots__/PulseReportPage.test.jsx.snap b/web-ui/src/pages/__snapshots__/PulseReportPage.test.jsx.snap index 0006bfbdf..9c45b2285 100644 --- a/web-ui/src/pages/__snapshots__/PulseReportPage.test.jsx.snap +++ b/web-ui/src/pages/__snapshots__/PulseReportPage.test.jsx.snap @@ -142,6 +142,47 @@ exports[`renders correctly 1`] = `
+
+ + + +

Date: Thu, 19 Dec 2024 13:25:41 -0600 Subject: [PATCH 4/6] Filter out zero count pie pieces. --- web-ui/src/pages/PulseReportPage.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 3ad40a8eb..066ff4eea 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -233,7 +233,9 @@ const PulseReportPage = () => { pieCounts[datapoint.externalScore - 1].value++; }); } - setPieChartData(pieCounts); + // Filter out data with a zero value so that the pie chart does not attempt + // to display them. + setPieChartData(pieCounts.filter((p) => p.value != 0)); setScoreChartData(scoreChartDataPoints.map(day => { const iScores = {}; From a50dc16f12531283b1a33106fde0400d9feee547 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 23 Dec 2024 08:37:28 -0600 Subject: [PATCH 5/6] Split the pie chart into two controlled by the toggle and colored like the other charts. --- web-ui/src/helpers/colors.js | 31 +++++ web-ui/src/helpers/index.js | 1 + web-ui/src/pages/PulseReportPage.jsx | 162 ++++++++++++++++++--------- 3 files changed, 143 insertions(+), 51 deletions(-) create mode 100644 web-ui/src/helpers/colors.js diff --git a/web-ui/src/helpers/colors.js b/web-ui/src/helpers/colors.js new file mode 100644 index 000000000..e2bf178af --- /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 0cebfe6af..bf72c325e 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 066ff4eea..cef70fc00 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -7,6 +7,7 @@ import { CartesianGrid, Legend, Pie, + Cell, PieChart, ResponsiveContainer, Tooltip, @@ -40,6 +41,7 @@ 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'; @@ -56,9 +58,8 @@ import './PulseReportPage.css'; // Recharts doesn't support using CSS variables, so we can't // easily use color variables defined in variables.css. const ociDarkBlue = '#2c519e'; -const ociLightBlue = '#76c8d4'; -// const ociOrange = '#f8b576'; // too light -const orange = '#b26801'; +//const ociLightBlue = '#76c8d4'; // not currently used +const ociOrange = '#f8b576'; const ScoreOption = { INTERNAL: 'Internal', @@ -121,7 +122,8 @@ const PulseReportPage = () => { const [selectedPulse, setSelectedPulse] = useState(null); const [showComments, setShowComments] = useState(false); const [teamMembers, setTeamMembers] = useState([]); - const [pieChartData, setPieChartData] = useState([]); + const [internalPieChartData, setInternalPieChartData] = useState([]); + const [externalPieChartData, setExternalPieChartData] = useState([]); /* // This generates random data to use in the line chart. @@ -220,22 +222,37 @@ const PulseReportPage = () => { } } - let pieCounts = [ - {name: "veryDissatisfied", value: 0}, - {name: "dissatisfied", value: 0}, - {name: "neutral", value: 0}, - {name: "satisfied", value: 0}, - {name: "verySatisfied", value: 0}, + 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 => { - pieCounts[datapoint.internalScore - 1].value++; - pieCounts[datapoint.externalScore - 1].value++; + internalPieCounts[datapoint.internalScore - 1].value++; }); } // Filter out data with a zero value so that the pie chart does not attempt // to display them. - setPieChartData(pieCounts.filter((p) => p.value != 0)); + 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 = {}; @@ -402,7 +419,7 @@ const PulseReportPage = () => { {(scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.EXTERNAL) && } @@ -448,16 +465,16 @@ const PulseReportPage = () => { }; 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: ociDarkBlue, }, + {key: "internalDissatisfied", stackId: "internal", color: pSBC(.05, ociDarkBlue), }, + {key: "internalNeutral", stackId: "internal", color: pSBC(.10, ociDarkBlue), }, + {key: "internalSatisfied", stackId: "internal", color: pSBC(.15, ociDarkBlue), }, + {key: "internalVerySatisfied", stackId: "internal", color: pSBC(.2, ociDarkBlue), }, + {key: "externalVeryDissatisfied", stackId: "external", color: pSBC(-.8, ociOrange), }, + {key: "externalDissatisfied", stackId: "external", color: pSBC(-.6, ociOrange), }, + {key: "externalNeutral", stackId: "external", color: pSBC(-.4, ociOrange), }, + {key: "externalSatisfied", stackId: "external", color: pSBC(-.2, ociOrange), }, + {key: "externalVerySatisfied", stackId: "external", color: ociOrange, }, ]; const labelToSentiment = (label) => { @@ -518,8 +535,8 @@ const PulseReportPage = () => { ); }; - const pulseScoresTitle = () => { - let title = "Pulse scores for"; + const sectionTitle = (prefix) => { + let title = `${prefix} for`; if (scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.INTERNAL) { title += ` "${ScoreOptionLabel[ScoreOption.INTERNAL]}"`; @@ -535,16 +552,16 @@ const PulseReportPage = () => { }; const pieLabelToSentiment = (label) => { - switch(label.toLowerCase()) { - case "verydissatisfied": + switch(label.replace("internal", "").replace("external", "")) { + case "VeryDissatisfied": return "😦"; - case "dissatisfied": + case "Dissatisfied": return "🙁"; - case "neutral": + case "Neutral": return "😐"; - case "satisfied": + case "Satisfied": return "🙂"; - case "verysatisfied": + case "VerySatisfied": return "😀"; } return "ERROR"; @@ -589,7 +606,10 @@ const PulseReportPage = () => { if (active && payload && payload.length) { return (
-

{titleWords(payload[0].name)} : {payload[0].value}

+

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

); } @@ -597,11 +617,17 @@ const PulseReportPage = () => { return null; }; + const pieSliceColor = (entry, index) => { + return value.key == entry.name).color} />; + }; + const pulseScoresChart = () => ( <> @@ -642,26 +668,60 @@ const PulseReportPage = () => { - - - } - /> - - - +
+ {(scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.INTERNAL) && +
+ + + } + /> + + {internalPieChartData.map(pieSliceColor)} + + + +
+ } + {(scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.EXTERNAL) && +
+ + + } + /> + + {externalPieChartData.map(pieSliceColor)} + + + +
+ } +
From 74f2cc5105828dd60fce10a7a71bb980b9e91e65 Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Mon, 23 Dec 2024 12:33:05 -0600 Subject: [PATCH 6/6] Adjust colors on pulse report --- web-ui/src/pages/PulseReportPage.jsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 7929f823a..4cf902f10 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -468,15 +468,15 @@ const PulseReportPage = () => { }; const dataInfo = [ - {key: "internalVeryDissatisfied", stackId: "internal", color: ociDarkBlue, }, - {key: "internalDissatisfied", stackId: "internal", color: pSBC(.05, ociDarkBlue), }, - {key: "internalNeutral", stackId: "internal", color: pSBC(.10, ociDarkBlue), }, - {key: "internalSatisfied", stackId: "internal", color: pSBC(.15, ociDarkBlue), }, - {key: "internalVerySatisfied", stackId: "internal", color: pSBC(.2, ociDarkBlue), }, - {key: "externalVeryDissatisfied", stackId: "external", color: pSBC(-.8, ociOrange), }, - {key: "externalDissatisfied", stackId: "external", color: pSBC(-.6, ociOrange), }, - {key: "externalNeutral", stackId: "external", color: pSBC(-.4, ociOrange), }, - {key: "externalSatisfied", stackId: "external", color: pSBC(-.2, ociOrange), }, + {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, }, ];