Public
Edited
Apr 24
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
screenshot20250424At091851 = FileAttachment("Screenshot 2025-04-24 at 09.23.48.png").image()
Insert cell
Insert cell
screenshot20250424At091951 = FileAttachment("Screenshot 2025-04-24 at 09.19.51.png").image()
Insert cell
Insert cell
screenshot20250424At092203 = FileAttachment("Screenshot 2025-04-24 at 09.22.03.png").image()
Insert cell
Insert cell
// Group reviews by semester and calculate sentiment for each period
reviewsByTime = {
// Group by semester
const groupedBySemester = d3.group(professorReviews, d => d.semester);
// Calculate aggregated sentiment for each semester
return Array.from(groupedBySemester, ([semester, semesterReviews]) => {
// Calculate average sentiment
const avgSentiment = {
POSITIVE: d3.mean(semesterReviews, d => d.sentiment.POSITIVE),
NEGATIVE: d3.mean(semesterReviews, d => d.sentiment.NEGATIVE),
NEUTRAL: d3.mean(semesterReviews, d => d.sentiment.NEUTRAL)
};
return {
semester,
reviews: semesterReviews.length,
sentiment: avgSentiment
};
}).sort((a, b) => {
// Sort by year and semester
const [yearA, semA] = a.semester.split(' ');
const [yearB, semB] = b.semester.split(' ');
return yearA === yearB ?
(semA === 'Spring' ? -1 : 1) :
(+yearA - +yearB);
});
}
Insert cell
timelineData = {
const data = [];
reviewsByTime.forEach(period => {
data.push({
semester: period.semester,
sentiment: "Positive",
value: period.sentiment.POSITIVE
});
data.push({
semester: period.semester,
sentiment: "Neutral",
value: period.sentiment.NEUTRAL
});
data.push({
semester: period.semester,
sentiment: "Negative",
value: period.sentiment.NEGATIVE
});
});
return data;
}
Insert cell
filteredReviews = {
let result = professorReviews;
// Apply sentiment filter
const sentimentValue = sentimentFilter.querySelector("select").value;
if (sentimentValue !== "all") {
result = result.filter(review => {
// The review.sentiment object has keys like "POSITIVE", "NEGATIVE", "NEUTRAL"
// Convert selected value to uppercase to match these keys
const selectedSentimentKey = sentimentValue.toUpperCase();
// Find the dominant sentiment (the one with highest value)
const dominantSentiment = Object.entries(review.sentiment)
.reduce((a, b) => a[1] > b[1] ? a : b);
// Match if the dominant sentiment type matches the filter
return dominantSentiment[0] === selectedSentimentKey;
});
}
// Apply course filter
const courseValue = sentimentFilter.querySelectorAll("select")[1].value;
if (courseValue !== "all") {
result = result.filter(review => review.courseId === courseValue);
}
return result;
}

Insert cell
calculatedSentiment = {
const professorReviews = reviews.filter(r => r.professorId === selectedProfessor.id);
if (professorReviews.length === 0) {
return { POSITIVE: 0, NEUTRAL: 0, NEGATIVE: 0 };
}
// Calculate average sentiment across all reviews
const totalSentiment = professorReviews.reduce((acc, review) => {
return {
POSITIVE: acc.POSITIVE + review.sentiment.POSITIVE,
NEUTRAL: acc.NEUTRAL + review.sentiment.NEUTRAL,
NEGATIVE: acc.NEGATIVE + review.sentiment.NEGATIVE
};
}, { POSITIVE: 0, NEUTRAL: 0, NEGATIVE: 0 });
// Divide by number of reviews to get average
return {
POSITIVE: totalSentiment.POSITIVE / professorReviews.length,
NEUTRAL: totalSentiment.NEUTRAL / professorReviews.length,
NEGATIVE: totalSentiment.NEGATIVE / professorReviews.length
};
}


Insert cell
combinedReviewText = {
const professorReviews = reviews.filter(r => r.professorId === selectedProfessor.id);
return professorReviews.map(r => r.text).join(" ");
}
Insert cell
professorReviews = reviews.filter(r => r.professorId === selectedProfessor.id)
Insert cell
actualReviewCount = professorReviews.length;
Insert cell
import {SentimentWidget} from "@laceyprojects/homework-5-design-a-react-widget"
Insert cell
import {Plot} from "@observablehq/plot"
Insert cell
d3 = require("d3@7")
Insert cell
professors = FileAttachment("professors.json").json()
Insert cell
reviews = FileAttachment("reviews.json").json()
Insert cell
courses = FileAttachment("courses.json").json()
Insert cell
html`<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
max-width: 900px;
margin: 0 auto;
padding: 0 20px;
}
.search-container {
display: flex;
margin-bottom: 20px;
max-width: 600px;
}
.search-container input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 16px;
}
.search-container button {
padding: 10px 20px;
background-color: #4a6fa5;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-size: 16px;
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 8px;
}
.filter-controls select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin-left: 10px;
}
.reviews-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.review-card {
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background-color: white;
border-left: 5px solid #ccc;
}
.positive-sentiment {
border-left-color: #4caf50;
}
.negative-sentiment {
border-left-color: #f44336;
}
.neutral-sentiment {
border-left-color: #9e9e9e;
}
.review-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
}
.review-text {
margin-bottom: 15px;
line-height: 1.5;
}
.sentiment-bar {
margin-top: 10px;
}
.sentiment-label {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
}
.sentiment-values {
height: 10px;
width: 100%;
background-color: #f5f5f5;
border-radius: 5px;
overflow: hidden;
display: flex;
}
.positive-value {
background-color: #4caf50;
height: 100%;
}
.neutral-value {
background-color: #9e9e9e;
height: 100%;
}
.negative-value {
background-color: #f44336;
height: 100%;
}
.sentiment-percentages {
display: flex;
justify-content: space-between;
font-size: 12px;
margin-top: 5px;
}
.comparison-controls {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.course-comparison-container {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
.course-column {
flex: 1;
padding: 15px;
background-color: #f9f9f9;
border-radius: 8px;
text-align: center;
}
.no-reviews, .no-data {
padding: 20px;
text-align: center;
background-color: #f9f9f9;
border-radius: 8px;
color: #666;
}
</style>`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more