You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mvfront-react/components/MeritProfile.tsx

249 lines
6.8 KiB

import {useEffect, useLayoutEffect, useRef, useState} from 'react';
import {GradeResultInterface, MeritProfileInterface} from '@services/type';
import {getMajorityGrade} from '@services/majorityJudgment';
interface ParamsInterface {
numVotes: number;
outgaugeThreshold: number;
}
interface GradeBarInterface {
grade: GradeResultInterface;
size: number;
index: number;
params: ParamsInterface;
}
const GradeBar = ({index, grade, size, params}: GradeBarInterface) => {
const width = `${size * 100}%`;
const textWidth = Math.floor(100 * size);
const left = `${(size * 100) / 2}%`;
const top = index % 2 ? '20px' : '-20px';
if (size < 0.001) {
return null;
}
return (
<div
className="h-100"
style={{
flexBasis: width,
backgroundColor: grade.color,
minHeight: '20px',
}}
>
{/* size < params.outgaugeThreshold ? (
<span
style={{
left: left,
top: top,
display: "relative",
backgroundColor: grade.color,
}}
>
{textWidth}%
</span>
) : (
<span>
{Math.floor(100 * size)}%
</span>
)
*/}
</div>
);
};
const DashedMedian = () => {
return (
<div
className="position-relative d-flex justify-content-center"
style={{top: '60px', height: '50px'}}
>
<div className="border h-100 border-1 border-dark border-opacity-75 border-dashed"></div>
</div>
);
};
const MajorityGrade = ({grade, left}) => {
// const spanRef = useRef<HTMLDivElement>();
const spanRef = useRef<HTMLDivElement>();
const [width, setWidth] = useState(0);
const offsetWidth = spanRef && spanRef.current && spanRef.current.offsetWidth;
useEffect(() => {
setTimeout(() => {
setWidth(spanRef.current.offsetWidth);
}, 100);
}, [offsetWidth]);
return (
<div
style={{left: `calc(${left * 100}% - ${width / 2}px)`}}
className="position-relative"
>
<div
ref={spanRef}
style={{
color: 'white',
backgroundColor: grade.color,
width: 'fit-content',
}}
className="p-2 fw-bold rounded-1 text-center"
>
{grade.name}
</div>
<div
style={{
width: 0,
height: 0,
position: 'relative',
left: `${width / 2 - 6}px`,
borderLeftWidth: 6,
borderRightWidth: 6,
borderTopWidth: 12,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderTopColor: grade.color,
color: 'transparent',
}}
></div>
</div>
);
};
interface MeritProfileBarInterface {
profile: MeritProfileInterface;
grades: Array<GradeResultInterface>;
}
const MeritProfileBar = ({profile, grades}: MeritProfileBarInterface) => {
const gradesByValue: {[key: number]: GradeResultInterface} = {};
grades.forEach((g) => (gradesByValue[g.value] = g));
const numVotes = Object.values(profile).reduce((a, b) => a + b, 0);
const values = grades.map((g) => g.value).sort();
const normalized = {};
values.forEach((v) => (normalized[v] = profile[v] / numVotes || 0));
// low values means great grade
// find the majority grade
const majorityValue = getMajorityGrade(normalized);
const majorityGrade = gradesByValue[majorityValue];
const proponentSizes = values
.filter((v) => v > majorityGrade.value)
.map((v) => normalized[v]);
const proponentWidth = proponentSizes.reduce((a, b) => a + b, 0);
const opponentSizes = values
.filter((v) => v < majorityGrade.value)
.map((v) => normalized[v]);
const opponentWidth = opponentSizes.reduce((a, b) => a + b, 0);
// is proponent higher than opposant?
const proponentMajority = proponentWidth > opponentWidth;
// for mobile phone, we outgauge earlier than on desktop
const innerWidth =
typeof window !== 'undefined' && window.innerWidth
? window.innerWidth
: 1000;
const params: ParamsInterface = {
outgaugeThreshold: innerWidth <= 760 ? 0.05 : 0.03,
numVotes,
};
return (
<>
<DashedMedian />
<MajorityGrade
grade={majorityGrade}
left={proponentWidth + normalized[majorityValue] / 2}
/>
<div className="d-flex">
{proponentWidth > 1e-5 ? (
<div
className={`d-flex border border-${proponentMajority ? 2 : 1
} border-success`}
style={{flexBasis: `${proponentWidth * 100}%`}}
>
{values
.filter((v) => v > majorityGrade.value)
.map((v) => {
const index = values.indexOf(v);
const size =
proponentWidth < 1e-3 ? 0 : normalized[index] / proponentWidth;
return (
<GradeBar
index={index}
params={params}
grade={gradesByValue[v]}
key={index}
size={size}
/>
);
})}
</div>) : (
<div
className="d-flex"
style={{flexBasis: `${proponentWidth * 100}%`}}
>
</div>)}
<div
className="border border-2 border-primary"
style={{flexBasis: `${normalized[majorityValue] * 100}%`}}
>
{values
.filter((v) => v === majorityGrade.value)
.map((v) => {
const index = values.indexOf(v);
return (
<GradeBar
index={index}
params={params}
grade={gradesByValue[v]}
key={index}
size={1}
/>
);
})}
</div>
{opponentWidth > 1e-5 ? (
<div
className={`d-flex border border-${proponentMajority ? 1 : 2
} border-danger`}
style={{flexBasis: `${opponentWidth * 100}%`}}
>
{values
.filter((v) => v < majorityGrade.value)
.map((v) => {
const index = values.indexOf(v);
const size =
opponentWidth < 1e-3 ? 0 : normalized[index] / opponentWidth;
return (
<GradeBar
index={index}
params={params}
grade={gradesByValue[v]}
key={index}
size={size}
/>
);
})}
</div>) : (
<div className="d-flex" style={{flexBasis: `${opponentWidth * 100}%`}} >
</div>
)}
</div>
{/* <div className='median dash'> </div> */}
</>
);
};
export default MeritProfileBar;