-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make the quarters offered tooltip more useful (#462)
* draft * courses with no term data * added to course and roadmap pages * lint * Update site/src/component/QuarterTooltip/Chart.tsx Co-authored-by: Jacob Sommer <[email protected]> --------- Co-authored-by: Jacob Sommer <[email protected]>
- Loading branch information
Showing
9 changed files
with
424 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import React from 'react'; | ||
import { ResponsiveBar, BarTooltipProps, BarDatum } from '@nivo/bar'; | ||
|
||
import ThemeContext from '../../style/theme-context'; | ||
import { type Theme } from '@nivo/core'; | ||
|
||
const colors = ['#E8966D', '#60A3D1', '#FFC7DF', '#F5D77F', '#E8966D', '#EBEBEB']; | ||
|
||
interface ChartProps { | ||
terms: string[]; | ||
} | ||
|
||
export default class Chart extends React.Component<ChartProps> { | ||
getTheme = (darkMode: boolean): Theme => { | ||
return { | ||
axis: { | ||
ticks: { | ||
text: { | ||
fill: darkMode ? '#eee' : '#333', | ||
}, | ||
}, | ||
legend: { | ||
text: { | ||
fill: darkMode ? '#eee' : '#333', | ||
}, | ||
}, | ||
}, | ||
}; | ||
}; | ||
|
||
/* | ||
* Create an array of objects to feed into the chart. | ||
* @return an array of JSON objects detailing the grades for each class | ||
*/ | ||
getTermData = (): BarDatum[] => { | ||
let fallCount = 0, | ||
winterCount = 0, | ||
springCount = 0; | ||
|
||
// for summer, count unique years rather than total terms (e.g. count SS1 2023 and SS2 2023 as one) | ||
const summerYears = new Set<string>(); | ||
|
||
this.props.terms.forEach((data) => { | ||
const [year, term] = data.split(' '); | ||
if (term === 'Fall') { | ||
fallCount++; | ||
} else if (term === 'Winter') { | ||
winterCount++; | ||
} else if (term === 'Spring') { | ||
springCount++; | ||
} else if (term.startsWith('Summer')) { | ||
summerYears.add(year); | ||
} | ||
}); | ||
|
||
return [ | ||
{ | ||
id: 'fall', | ||
label: 'Fall', | ||
fall: fallCount, | ||
color: '#E8966D', | ||
}, | ||
{ | ||
id: 'winter', | ||
label: 'Winter', | ||
winter: winterCount, | ||
color: '#2484C6', | ||
}, | ||
{ | ||
id: 'spring', | ||
label: 'Spring', | ||
spring: springCount, | ||
color: '#FFC7DF', | ||
}, | ||
{ | ||
id: 'summer', | ||
label: 'Summer', | ||
summer: summerYears.size, | ||
color: '#F9CE50', | ||
}, | ||
]; | ||
}; | ||
|
||
tooltipStyle: Theme = { | ||
tooltip: { | ||
container: { | ||
background: 'rgba(0,0,0,.87)', | ||
color: '#ffffff', | ||
fontSize: '1.2rem', | ||
outline: 'none', | ||
margin: 0, | ||
padding: '0.25em 0.5em', | ||
borderRadius: '2px', | ||
}, | ||
}, | ||
}; | ||
|
||
/* | ||
* Indicate how the tooltip should look like when users hover over the bar | ||
* Code is slightly modified from: https://codesandbox.io/s/nivo-scatterplot- | ||
* vs-bar-custom-tooltip-7u6qg?file=/src/index.js:1193-1265 | ||
* @param event an event object recording the mouse movement, etc. | ||
* @return a JSX block styling the chart | ||
*/ | ||
styleTooltip = (props: BarTooltipProps<BarDatum>) => { | ||
return ( | ||
<div style={this.tooltipStyle.tooltip?.container}> | ||
<strong> | ||
{props.label}: {props.data[props.label]} | ||
</strong> | ||
</div> | ||
); | ||
}; | ||
|
||
/* | ||
* Display the grade distribution chart. | ||
* @return a JSX block rendering the chart | ||
*/ | ||
render() { | ||
const data = this.getTermData(); | ||
|
||
// greatestCount calculates the upper bound of the graph (i.e. the greatest number of students in a single grade) | ||
const greatestCount = data.reduce( | ||
(max, term) => ((term[term.id] as number) > max ? (term[term.id] as number) : max), | ||
0, | ||
); | ||
|
||
// The base marginX is 30, with increments of 5 added on for every order of magnitude greater than 100 to accomadate for larger axis labels (1,000, 10,000, etc) | ||
// For example, if greatestCount is 5173 it is (when rounding down (i.e. floor)), one magnitude (calculated with log_10) greater than 100, therefore we add one increment of 5px to our base marginX of 30px | ||
// Math.max() ensures that we're not finding the log of a non-positive number | ||
const marginX = 30 + 5 * Math.floor(Math.log10(Math.max(100, greatestCount) / 100)); | ||
|
||
return ( | ||
<> | ||
<ThemeContext.Consumer> | ||
{({ darkMode }) => ( | ||
<ResponsiveBar | ||
data={data} | ||
keys={['fall', 'winter', 'spring', 'summer']} | ||
margin={{ | ||
top: 25, | ||
right: marginX, | ||
bottom: 25, | ||
left: marginX, | ||
}} | ||
layout="vertical" | ||
axisBottom={{ | ||
tickSize: 5, | ||
tickPadding: 5, | ||
tickRotation: 0, | ||
legend: 'Term', | ||
legendPosition: 'middle', | ||
legendOffset: 36, | ||
}} | ||
axisLeft={{ | ||
tickValues: Array.from({ length: greatestCount }, (_, i) => i + 1), // integers from 1 to max | ||
}} | ||
enableLabel={false} | ||
colors={colors} | ||
theme={this.getTheme(darkMode)} | ||
tooltipLabel={(datum) => String(datum.id)} | ||
tooltip={this.styleTooltip} | ||
/> | ||
)} | ||
</ThemeContext.Consumer> | ||
</> | ||
); | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
site/src/component/QuarterTooltip/CourseQuarterIndicator.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
.quarter-indicator-container { | ||
display: flex; | ||
margin-left: auto; | ||
float: right; | ||
|
||
.quarter-indicator-row { | ||
display: flex; | ||
justify-content: end; | ||
max-height: 36px; | ||
|
||
.emoji-xs { | ||
font-size: 16px; | ||
} | ||
|
||
.emoji-sm { | ||
font-size: 18px; | ||
} | ||
|
||
.emoji-lg { | ||
font-size: 24px; | ||
} | ||
} | ||
} | ||
|
||
.quarter-tooltip { | ||
.emoji-xs, | ||
.emoji-sm, | ||
.emoji-lg { | ||
font-size: 14px; | ||
} | ||
|
||
.emoji-label { | ||
margin-left: 4px; | ||
} | ||
|
||
.not-offered-text { | ||
font-weight: bold; | ||
margin-bottom: 6px; | ||
} | ||
|
||
.tooltip-column { | ||
display: flex; | ||
flex-direction: column; | ||
margin: 4px; | ||
} | ||
|
||
.tooltip-chart-section { | ||
margin: 16px 0; | ||
} | ||
|
||
.term-chart-container { | ||
display: inline-flex; | ||
height: 200px; | ||
width: 300px; | ||
} | ||
} | ||
|
||
@media only screen and (max-width: 400px) { | ||
.quarter-indicator-container { | ||
.quarter-indicator-row { | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: start; | ||
} | ||
} | ||
} |
147 changes: 147 additions & 0 deletions
147
site/src/component/QuarterTooltip/CourseQuarterIndicator.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { FC } from 'react'; | ||
import { Label, Popup } from 'semantic-ui-react'; | ||
import './CourseQuarterIndicator.scss'; | ||
import Chart from './Chart'; | ||
|
||
interface CourseQuarterIndicatorProps { | ||
terms: string[]; | ||
size: 'xs' | 'sm' | 'lg'; | ||
} | ||
|
||
const CourseQuarterIndicator: FC<CourseQuarterIndicatorProps> = (props) => { | ||
const emojiSize = `emoji-${props.size}`; | ||
|
||
// if currently in fall quarter, previous academic year still includes the current | ||
const prevYear = new Date().getMonth() > 9 ? new Date().getFullYear() : new Date().getFullYear() - 1; | ||
|
||
// show in order of fall, winter, spring, summer | ||
const termsInOrder = props.terms.slice().reverse(); | ||
|
||
const summerOfferings = [ | ||
termsInOrder.includes(`${prevYear} Summer10wk`) && 'Summer Session (10 Week)', | ||
termsInOrder.includes(`${prevYear} Summer1`) && 'Summer Session 1', | ||
termsInOrder.includes(`${prevYear} Summer2`) && 'Summer Session 2', | ||
]; | ||
|
||
// check if the course was offered in the previous academic year at any term | ||
const offeredLastYear = | ||
termsInOrder.includes(`${prevYear - 1} Fall`) || | ||
termsInOrder.includes(`${prevYear} Winter`) || | ||
termsInOrder.includes(`${prevYear} Spring`) || | ||
// if the course was offered in any summer session from last year | ||
summerOfferings.some((term) => term); | ||
|
||
// find min and max year in term range | ||
const years = termsInOrder.map((term) => parseInt(term.split(' ')[0])); | ||
const minYear = years.reduce((min, val) => Math.min(min, val), prevYear); | ||
const maxYear = years.reduce((max, val) => Math.max(max, val), 0); | ||
|
||
return ( | ||
<div className="quarter-indicator-container"> | ||
<Popup | ||
trigger={ | ||
offeredLastYear ? ( | ||
// icons to show which terms were offered last year | ||
<span className="quarter-indicator-row"> | ||
{termsInOrder.includes(`${prevYear - 1} Fall`) && ( | ||
<span> | ||
<span className={emojiSize}>🍂</span> | ||
</span> | ||
)} | ||
|
||
{termsInOrder.includes(`${prevYear} Winter`) && ( | ||
<span> | ||
<span className={emojiSize}>❄️</span> | ||
</span> | ||
)} | ||
|
||
{termsInOrder.includes(`${prevYear} Spring`) && ( | ||
<span> | ||
<span className={emojiSize}>🌸</span> | ||
</span> | ||
)} | ||
|
||
{ | ||
// summer icon shows if there was any summer session offering | ||
summerOfferings.some((term) => term) && ( | ||
<span> | ||
<span className={emojiSize}>☀️</span> | ||
</span> | ||
) | ||
} | ||
</span> | ||
) : ( | ||
// no offerings from last year, no icons to show | ||
<Label circular color="grey" empty /> | ||
) | ||
} | ||
content={ | ||
<div className="quarter-tooltip"> | ||
{props.terms.length ? ( | ||
<div> | ||
{offeredLastYear ? ( | ||
// legend to show terms corresponding to the icons | ||
<div className="tooltip-column"> | ||
<h5 style={{ marginBottom: '4px' }}>Last offered in:</h5> | ||
{termsInOrder.includes(`${prevYear - 1} Fall`) && ( | ||
<div> | ||
<span className={emojiSize}>🍂</span> | ||
<span className="emoji-label">Fall {prevYear - 1}</span> | ||
</div> | ||
)} | ||
|
||
{termsInOrder.includes(`${prevYear} Winter`) && ( | ||
<div> | ||
<span className={emojiSize}>❄️</span> | ||
<span className="emoji-label">Winter {prevYear}</span> | ||
</div> | ||
)} | ||
|
||
{termsInOrder.includes(`${prevYear} Spring`) && ( | ||
<div> | ||
<span className={emojiSize}>🌸</span> | ||
<span className="emoji-label">Spring {prevYear}</span> | ||
</div> | ||
)} | ||
|
||
{summerOfferings.some((term) => term) && ( | ||
<div> | ||
<span className={emojiSize}>☀️</span> | ||
<span className="emoji-label"> | ||
{/* list out summer sessions offered */} | ||
{summerOfferings.filter((term) => term).join(', ')} {prevYear} | ||
</span> | ||
</div> | ||
)} | ||
</div> | ||
) : ( | ||
// hide legend if course has term data, but not for the previous year | ||
<p className="not-offered-text"> | ||
This course was not offered in the {prevYear - 1}-{prevYear} academic year. | ||
</p> | ||
)} | ||
|
||
{/* chart of past term offerings */} | ||
<div className="tooltip-chart-section"> | ||
<h5 style={{ textAlign: 'center' }}> | ||
Past Offerings ({minYear !== maxYear ? `${minYear} - ${maxYear}` : `${minYear}`}) | ||
</h5> | ||
<div className="term-chart-container chart"> | ||
<Chart terms={props.terms} /> | ||
</div> | ||
</div> | ||
</div> | ||
) : ( | ||
// hide legend and chart if there is no term data at all | ||
<p className="not-offered-text">This course has not been offered in any recent years.</p> | ||
)} | ||
</div> | ||
} | ||
basic | ||
position="bottom right" | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default CourseQuarterIndicator; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.