Skip to content

Commit

Permalink
feat: add results viz for ratings
Browse files Browse the repository at this point in the history
work in progress
  • Loading branch information
swouf committed Nov 6, 2024
1 parent 9b8706b commit b46ecd3
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 42 deletions.
3 changes: 1 addition & 2 deletions src/modules/common/response/Response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ const Response: FC<ResponseProps> = ({
variant="outlined"
sx={{
width: '100%',
// backgroundColor: highlight ? 'hsla(0, 100%, 90%, 0.3)' : 'white',
}}
data-cy={RESPONSE_CY}
>
Expand Down Expand Up @@ -217,7 +216,7 @@ const Response: FC<ResponseProps> = ({
</Box>
</CardContent>
{renderEvaluationComponent()}
{showRatings && <RatingsVisualization />}
{showRatings && <RatingsVisualization responseId={id} />}
{typeof nbrOfVotes !== 'undefined' && <Votes votes={nbrOfVotes} />}
{showActions && (
<>
Expand Down
98 changes: 65 additions & 33 deletions src/modules/common/response/visualization/RatingsVisualization.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,68 @@
import { useTranslation } from 'react-i18next';

import { TRANSLATIONS_NS } from '@/config/i18n';

const RatingsVisualization = (): JSX.Element => {
const { t } = useTranslation(TRANSLATIONS_NS);
// const { activity } = useSettings();
// const { evaluationType } = activity;
// const { getRatingsForResponse } = useRatings(evaluationType);

// const ratings = useMemo(
// () => getRatingsForResponse(response.id),
// [getRatingsForResponse, response],
// );

// if (evaluationType === EvaluationType.DimensionsOfGIRating) {
// return (
// <DimensionsOfGlobalIssue
// ratings={ratings as RatingsAppData<DimensionsOfGIRatings>[]}
// />
// );
// }
// if (evaluationType === EvaluationType.SFERARating) {
// return <SFERAViz ratings={ratings as RatingsAppData<SFERARating>[]} />;
// }
// if (evaluationType === EvaluationType.UsefulnessNoveltyRating) {
// return (
// <UsefulnessNovelty
// ratings={ratings as RatingsAppData<UsefulnessNoveltyRatings>[]}
// />
// );
// }
return <>{t('NO_VISUALIZATION')}</>;
import { FC, useEffect, useState } from 'react';

import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack';

import { RatingData } from '@/config/appDataTypes';
import { useRatingsContext } from '@/modules/context/RatingsContext';

import CircularIndicator from './indicators/CircularIndicator';

interface RatingsVisualizationProps {
responseId: string;
}

const RatingsVisualization: FC<RatingsVisualizationProps> = ({
responseId,
}): JSX.Element => {
const {
ratings: ratingsDef,
getRatingsStatsForResponse,
ratingsThresholds,
} = useRatingsContext();

const [ratings, setRatings] = useState<RatingData['ratings'] | undefined>(
undefined,
);

useEffect(() => {
getRatingsStatsForResponse(responseId).then((d) => setRatings(d));
}, [getRatingsStatsForResponse, responseId]);

if (typeof ratingsDef === 'undefined' || typeof ratings === 'undefined') {
// TODO: Make that look good.
return <CircularProgress />;
}

const nbrRatings = ratingsDef?.length ?? 0;

return (
<Stack
direction="row"
spacing={2}
alignItems="stretch"
justifyContent="center"
m={2}
>
{ratingsDef.map((singleRatingDefinition, index) => {
const { name } = singleRatingDefinition;
if (ratings) {
const result = ratings[index];
console.info(`For aspect ${name}, result is:`, result.value);
return (
<CircularIndicator
key={index}
value={result.value}
thresholds={ratingsThresholds}
label={name}
width={`${100 / nbrRatings}%`}
/>
);
}
return <CircularProgress key={index} />;
})}
</Stack>
);
};

export default RatingsVisualization;
119 changes: 118 additions & 1 deletion src/modules/context/RatingsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
import { FC, createContext, useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import { useLocalContext } from '@graasp/apps-query-client';
import { AppDataVisibility } from '@graasp/sdk';

import { AppDataTypes, RatingAppData, RatingData } from '@/config/appDataTypes';
import { TRANSLATIONS_NS } from '@/config/i18n';
import { EvaluationParameters } from '@/interfaces/evaluation';

import { useAppDataContext } from './AppDataContext';

type ThresholdType = {
value: number;
label: string;
color: 'success' | 'warning' | 'error' | 'primary';
};

type RatingsContextType = {
ratingsName: EvaluationParameters['ratingsName'];
ratings: EvaluationParameters['ratings'];
allRatings: Array<RatingAppData>;
myRatings: Array<RatingAppData>;
rate: (rating: RatingData) => Promise<void>;
removeRatingsFor: (responseId: string) => void;
getAllRatingsForResponse: (
responseId: string,
) => Promise<Array<RatingAppData> | undefined>;
getRatingsStatsForResponse: (
responseId: string,
) => Promise<RatingData['ratings'] | undefined>;
ratingsThresholds: ThresholdType[];
};
const defaultContextValue: RatingsContextType = {
ratingsName: undefined,
Expand All @@ -23,6 +38,9 @@ const defaultContextValue: RatingsContextType = {
myRatings: [],
rate: () => Promise.resolve(),
removeRatingsFor: () => Promise.resolve(),
getAllRatingsForResponse: () => Promise.resolve(undefined),
getRatingsStatsForResponse: () => Promise.resolve(undefined),
ratingsThresholds: [],
};

const RatingsContext = createContext<RatingsContextType>(defaultContextValue);
Expand All @@ -36,6 +54,7 @@ export const RatingsProvider: FC<RatingsContextProps> = ({
evaluationParameters,
children,
}) => {
const { t } = useTranslation(TRANSLATIONS_NS);
const { accountId } = useLocalContext();

const { appData, postAppData, deleteAppData, patchAppData } =
Expand Down Expand Up @@ -101,6 +120,91 @@ export const RatingsProvider: FC<RatingsContextProps> = ({
[deleteAppData, findRatingsFor],
);

const getAllRatingsForResponse = useCallback(
async (responseId: string): Promise<RatingAppData[] | undefined> =>
allRatings?.filter(({ data }) => data.responseRef === responseId),
[allRatings],
);

const ratingsLevels = useMemo(() => {
const levels = new Map(ratings.map((r) => [r.name, r.levels]));
return levels;
}, [ratings]);

const computeMeanRatings = (
accumulatedRatings: RatingData['ratings'],
currentRating: RatingData['ratings'],
_index: number,
array: RatingData['ratings'][],
): RatingData['ratings'] => {
const nbrRatings = array.length;
const newAccumulatedRatings: RatingData['ratings'] = currentRating.map(
(r, index) => {
let prevVal = 0;
if (accumulatedRatings.length > index) {
prevVal = accumulatedRatings[index].value;
}
const value =
r.value / ((ratingsLevels.get(r.name) ?? 1) * nbrRatings) + prevVal;
console.debug(
`Accumulating for aspect ${r.name}, current value = ${value} (prevVal=${prevVal})`,
);
return {
...r,
value,
};
},
);
return newAccumulatedRatings;
};

const getRatingsStatsForResponse = useCallback(
async (responseId: string): Promise<RatingData['ratings'] | undefined> => {
const ratingsForResponse = await getAllRatingsForResponse(responseId);
if (typeof ratingsForResponse !== 'undefined') {
const extractedRatings = ratingsForResponse.map(
({ data }) => data.ratings,
);

const initialVal = extractedRatings[0].map((r) => ({
...r,
value: 0,
}));

const accumulatedRatings = extractedRatings.reduce(
computeMeanRatings,
initialVal,
);
console.debug('Accumulated ratings:');
console.debug(accumulatedRatings);
return accumulatedRatings;
}
return undefined;
},
[computeMeanRatings, getAllRatingsForResponse],
);

const ratingsThresholds: ThresholdType[] = useMemo(
() => [
{
value: 0,
color: 'error',
label: t('BAD'),
},
{
value: 1 / 3,
color: 'warning',
label: t('OKAY'),
},
{
value: 2 / 3,
color: 'success',
label: t('GOOD'),
},
],
[t],
);

const contextValue = useMemo(
() => ({
ratingsName,
Expand All @@ -109,8 +213,21 @@ export const RatingsProvider: FC<RatingsContextProps> = ({
myRatings,
rate,
removeRatingsFor,
getAllRatingsForResponse,
getRatingsStatsForResponse,
ratingsThresholds,
}),
[allRatings, myRatings, rate, ratings, ratingsName, removeRatingsFor],
[
allRatings,
getAllRatingsForResponse,
getRatingsStatsForResponse,
myRatings,
rate,
ratings,
ratingsName,
ratingsThresholds,
removeRatingsFor,
],
);
return (
<RatingsContext.Provider value={contextValue}>
Expand Down
38 changes: 38 additions & 0 deletions src/modules/results/RatingsResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { FC } from 'react';

import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';

import Response from '@/modules/common/response/Response';

import ExportResponsesButton from '../common/ExportRepsonsesButton';
import { useActivityContext } from '../context/ActivityContext';

type RatingsResultsProps = unknown;

const RatingsResults: FC<RatingsResultsProps> = () => {
const { allResponses } = useActivityContext();
// const sortedResponses = useMemo(
// () => sortResponsesByNumberOfVote(allResponses, allVotes),
// [allResponses, allVotes],
// );
return (
<Stack
direction="column"
justifyItems="center"
alignItems="center"
spacing={2}
>
<Grid container spacing={2}>
{allResponses.map((response) => (
<Grid item key={response.id} xl={2} sm={4} xs={6}>
<Response response={response} showRatings />
</Grid>
))}
</Grid>
<ExportResponsesButton responses={allResponses} />
</Stack>
);
};

export default RatingsResults;
24 changes: 18 additions & 6 deletions src/modules/results/ResultsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ import Pausable from '../common/Pausable';
import { RatingsProvider } from '../context/RatingsContext';
import { VoteProvider } from '../context/VoteContext';
import NoEvaluationResults from './NoEvaluationResults';
import RatingsResults from './RatingsResults';
import VoteResults from './VoteResults';

type ResultsViewProps = unknown;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ResultsView = (props: ResultsViewProps): JSX.Element => {
const { currentStep } = useSteps();
const { currentStep, previousStep } = useSteps();
const resultsType = currentStep?.resultsType ?? DEFAULT_EVALUATION_TYPE;
/**
* With this mechanism, bear in mind that the configuration of
* the previous step is used to determine the evaluation parameters.
* Therefore, one need to have the results step immediately after
* the evaluation step.
*/
const evaluationParameters = previousStep?.evaluationParameters;

const renderResultsContext = (): JSX.Element | null => {
switch (resultsType) {
Expand All @@ -28,11 +36,15 @@ const ResultsView = (props: ResultsViewProps): JSX.Element => {
</VoteProvider>
);
case EvaluationType.Rate:
return (
<RatingsProvider>
<p>Nothing.</p>
</RatingsProvider>
);
if (evaluationParameters) {
return (
<RatingsProvider evaluationParameters={evaluationParameters}>
<RatingsResults />
</RatingsProvider>
);
}
return <NoEvaluationResults />;

default:
return <NoEvaluationResults />;
}
Expand Down

0 comments on commit b46ecd3

Please sign in to comment.