Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added alternate stats displaying the count for each rating #286

Closed
wants to merge 19 commits into from
Closed
165 changes: 117 additions & 48 deletions client/src/components/CourseInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,131 @@
import { ExternalLink } from 'react-feather';

import { HiChartBar, HiChartPie } from 'react-icons/hi';
import { useState } from 'react';
import { Course } from '../model/Course';
import { CourseTerms } from './CourseTerms';
import { RatingInfo } from './RatingInfo';

type ChartsProps = {
numReviews?: number;
rating: number;
difficulty: number;
};

const Charts = ({ numReviews, rating, difficulty }: ChartsProps) => {
if (numReviews === undefined) return null;

return numReviews ? (
<>
<RatingInfo title={'Rating'} rating={rating} />
<RatingInfo title={'Difficulty'} rating={difficulty} />
</>
) : (
<div className='w-[50%] text-left text-gray-700 dark:text-gray-200 md:text-center'>
No reviews have been left for this course yet. Be the first!
</div>
);
};
import { Review } from '../model/Review';
import { countRatings } from '../lib/utils';

type CourseInfoProps = {
course: Course;
rating: number;
difficulty: number;
numReviews?: number;
reviews: Review[];
};

export const CourseInfo = ({
course,
rating,
difficulty,
numReviews,
}: CourseInfoProps) => {
export const CourseInfo = ({ course, reviews }: CourseInfoProps) => {
const ratingMap: number[] = countRatings('rating', reviews);
const difficultyMap: number[] = countRatings('difficulty', reviews);
const numReviews = reviews.length;

const [chartType, setChartType] = useState<'pie' | 'histogram'>('pie');

return (
<div className='flex w-full flex-row rounded-md bg-slate-50 p-6 dark:bg-neutral-800 md:mt-10'>
<div className='m-4 space-y-3 md:m-4 md:w-1/2'>
<div className='flex flex-row space-x-2 align-middle'>
<h1 className='text-4xl font-semibold text-gray-800 dark:text-gray-200'>
{course._id}
</h1>
{course.url ? (
<a
href={course.url}
className='my-auto dark:text-gray-200'
target='_blank'
>
<ExternalLink
size={20}
className='ml-1 transition-colors duration-300 hover:stroke-red-600'
<div className='flex justify-center'>
<div className='mx-8 flex w-screen flex-row rounded-md bg-slate-50 p-6 dark:bg-neutral-800 md:mt-10'>
<div className='flex flex-1 flex-col md:flex-row'>
<div className='m-4 flex w-fit flex-col space-y-3 md:m-4 md:w-1/2'>
<div className='flex flex-row space-x-2 align-middle'>
<h1 className='break-words text-4xl font-semibold text-gray-800 dark:text-gray-200'>
{course._id}
</h1>
{course.url ? (
<a
href={course.url}
className='my-auto dark:text-gray-200'
target='_blank'
>
<ExternalLink
size={20}
className='ml-1 transition-colors duration-300 hover:stroke-red-600'
/>
</a>
) : null}
</div>
<h2 className='text-3xl text-gray-800 dark:text-gray-200'>
{course.title}
</h2>
<div className='m-4 mx-auto flex w-full flex-col items-center justify-center space-y-3 md:hidden'>
<RatingInfo
title={'Rating'}
chartType={chartType}
ratings={ratingMap}
numReviews={numReviews}
/>
<RatingInfo
title={'Difficulty'}
chartType={chartType}
ratings={difficultyMap}
numReviews={numReviews}
/>
<div className='mx-auto flex flex-row md:hidden'>
<HiChartPie
className={classNames(
'm-2 mr-2 cursor-pointer ',
chartType === 'pie'
? 'text-red-600 dark:text-red-600'
: 'text-neutral-800 dark:text-gray-200'
)}
onClick={() => setChartType('pie')}
size={30}
/>
<HiChartBar
className={classNames(
'm-2 mr-2 cursor-pointer ',
chartType === 'histogram'
? 'text-red-600 dark:text-red-600'
: 'text-neutral-800 dark:text-gray-200'
)}
onClick={() => setChartType('histogram')}
size={30}
/>
</div>
</div>
<CourseTerms course={course} variant='large' />
<p className='break-words text-gray-500 dark:text-gray-400'>
{course.description}
</p>
<p className='text-sm text-gray-500 dark:text-gray-400'>
{numReviews} reviews
</p>
</div>
<div className='mx-auto my-auto flex w-1/2 flex-col'>
<div className='m-4 mx-auto hidden h-full w-full flex-col items-center justify-center space-y-5 md:flex'>
<RatingInfo
title={'Rating'}
chartType={chartType}
ratings={ratingMap}
numReviews={numReviews}
/>
<RatingInfo
chartType={chartType}
title={'Difficulty'}
ratings={difficultyMap}
numReviews={numReviews}
/>
</div>
<div className='mx-auto mt-auto hidden md:flex'>
<HiChartPie
className={classNames(
'm-2 mr-2 cursor-pointer ',
chartType === 'pie'
? 'text-red-600 dark:text-red-600'
: 'text-neutral-800 dark:text-gray-200'
)}
onClick={() => setChartType('pie')}
size={30}
/>
<HiChartBar
className={classNames(
'm-2 ml-2 cursor-pointer ',
chartType === 'histogram'
? 'text-red-600 dark:text-red-600'
: 'text-neutral-800 dark:text-gray-200'
)}
onClick={() => setChartType('histogram')}
size={30}
/>
</a>
) : null}
</div>
</div>
</div>
<h2 className='text-3xl text-gray-800 dark:text-gray-200'>
{course.title}
Expand Down
61 changes: 61 additions & 0 deletions client/src/components/RatingHistogram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
Chart,
LinearScale,
CategoryScale,
BarElement,
Tooltip,
} from 'chart.js';
import { Bar } from 'react-chartjs-2';

Chart.register(LinearScale, CategoryScale, BarElement, Tooltip);

export const RatingHistogram = ({ ratings }: { ratings: number[] }) => {
const data = {
labels: [5, 4, 3, 2, 1],
datasets: [
{
data: ratings,
backgroundColor: '#dc2626',
borderWidth: 0,
},
],
};

const options = {
indexAxis: 'y',
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: true,
callbacks: {
label: (context: any) => {
const count = ratings[context.dataIndex];
return `Number of ratings: ${count}`;
},
},
},
},
scales: {
x: {
beginAtZero: true,
max: Math.max(...ratings) + 1,
},
y: {
beginAtZero: true,
ticks: {
precision: 0,
},
},
},
responsive: true,
maintainAspectRatio: false,
} as const;

return (
<div className='h-full max-w-full'>
<Bar data={data} options={options} />
</div>
);
};
47 changes: 40 additions & 7 deletions client/src/components/RatingInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
import { RatingPieChart } from './RatingPieChart';
import { RatingHistogram } from './RatingHistogram';
import _ from 'lodash';

type RatingInfoProps = {
export const RatingInfo = ({
title,
chartType,
numReviews,
ratings,
content,
}: {
title: string;
rating: number;
};
chartType: 'pie' | 'histogram';
numReviews: number;
ratings: number[];
content?: string;
}) => {
const averageRating =
_.sum(ratings.map((value, index) => value * (index + 1))) / numReviews;
const pieChart = (
<div className='mx-1 pt-1'>
<RatingPieChart averageRating={averageRating} />
</div>
);

const histogramChart = (
<div className='mx-4'>
<RatingHistogram ratings={ratings} />
</div>
);

export const RatingInfo = ({ title, rating }: RatingInfoProps) => {
return (
<div className='w-full lg:w-3/4'>
<h1 className='text-center text-xl font-semibold text-gray-800 dark:text-gray-200'>
<div className='w-full md:px-8 lg:w-3/4 lg:p-0'>
<h1 className='mb-3 text-center text-xl font-semibold text-gray-800 dark:text-gray-200'>
{title}
</h1>
<RatingPieChart rating={rating} />
{numReviews > 0 ? (
chartType === 'pie' ? (
pieChart
) : (
histogramChart
)
) : (
<div className='text-left text-gray-700 dark:text-gray-200 md:text-center'>
{content ?? 'No reviews have been left yet. Be the first!'}
</div>
)}
</div>
);
};
55 changes: 34 additions & 21 deletions client/src/components/RatingPieChart.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
import { PieChart } from 'react-minimal-pie-chart';
import { Chart, ArcElement } from 'chart.js';
import { Doughnut } from 'react-chartjs-2';

type RatingPieChartProps = {
rating: number;
};
Chart.register(ArcElement);

export const RatingPieChart = ({
averageRating,
}: {
averageRating: number;
}) => {
const data = {
labels: ['Average Rating', 'Remaining'],
datasets: [
{
data: [averageRating, 5 - averageRating],
backgroundColor: ['#dc2626', '#e4e4e7'],
borderWidth: 0,
},
],
};

const options = {
plugins: {
legend: {
display: false,
},
},
cutout: '67%', // The size of the inner circle
maintainAspectRatio: false,
responsive: true,
};

export const RatingPieChart = ({ rating }: RatingPieChartProps) => {
return (
<div className='flex flex-col'>
<div className='relative z-10 mx-auto my-5 flex h-1/2 w-3/5 rounded-full '>
<PieChart
data={[
{
value: rating,
color: '#dc2626',
},
{
value: 5 - rating,
color: '#e4e4e7',
},
]}
lineWidth={20}
/>
<div className='absolute inset-0 z-20 m-auto flex w-10/12 items-center justify-center text-xl font-semibold text-gray-700 dark:text-gray-300'>
{Math.round(rating * 100) / 100} / 5
<div className='relative z-10 mx-auto flex h-1/2 w-3/5 rounded-full'>
<Doughnut data={data} options={options} />
<div className='text-l absolute inset-0 z-20 mx-auto my-auto flex w-10/12 items-center justify-center font-semibold text-gray-700 dark:text-gray-300'>
{Math.round(averageRating * 100) / 100} / 5
</div>
</div>
</div>
Expand Down
21 changes: 21 additions & 0 deletions client/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Course } from '../model/Course';
import { Instructor } from '../model/Instructor';
import { Schedule } from '../model/Schedule';
import { Review } from '../model/Review';

export const uniqueTermInstructors = (course: Course) => {
const termInstructors = course.instructors.filter((i) =>
Expand Down Expand Up @@ -72,3 +73,23 @@ export const sortSchedulesByBlocks = (schedules: Schedule[]) => {
export const getUrl = (): string => {
return import.meta.env.VITE_API_URL ?? '';
};


export const countRatings = (
type: 'rating' | 'difficulty',
reviews: Review[]
) => {
const ratings: number[] = [0, 0, 0, 0, 0];
const target = (r: Review) => {
switch (type) {
case 'rating':
return r.rating;
case 'difficulty':
return r.difficulty;
}
};

reviews.forEach((r: Review) => ratings[target(r) - 1]++);

return ratings;
};
Loading
Loading