Public
Edited
Apr 24
Importers
Insert cell
Insert cell
Insert cell
Insert cell
# Sentiment Analysis Widget Documentation

## Overview
The Sentiment Analysis Widget is a reactive tool for analyzing and visualizing sentiment in professor reviews. It analyzes text input in real time and presents results through an interactive donut chart visualization.

## Key Features
- 🔄 Real-time sentiment analysis
- 📊 Interactive donut chart visualization
- 🔍 Sentiment filtering options
- 📱 Responsive design
- ⚡ Reactive updates
- 📈 Detailed statistics display

## Installation

### Step-by-Step Installation in Observable

1. Create a new Observable notebook
2. Add a new cell and paste the import statement:
```javascript
import {SentimentWidget} from "@laceyprojects/homework-5-design-a-react-widget"
```
3. In a new cell, create the widget:
```javascript
viewof sentiment = SentimentWidget()
```
4. In another cell, display the sentiment value:
```javascript
sentiment
```

### NPM Installation
```bash
# Install required dependencies
npm install d3@7 @xenova/transformers@2.15.0
```

### Customized Implementation
```javascript
// Create a customized widget with specific options
viewof customSentiment = SentimentWidget(null, {
width: 800,
height: 500,
chartType: "donut"
})
```

## Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| value | object | null | Initial sentiment value |
| width | number | 600 | Widget width in pixels |
| height | number | 400 | Widget height in pixels |
| chartType | string | "donut" | Type of chart to display |

## Return Value Structure
The widget returns a sentiment object with the following structure:
```javascript
{
POSITIVE: number, // Score between 0-1
NEGATIVE: number, // Score between 0-1
NEUTRAL: number // Score between 0-1
}
```

### Sample Reviews for Testing

#### Positive Review
```text
"Professor Smith is an excellent teacher who explains complex concepts clearly.
The course was challenging but rewarding."
```

#### Negative Review
```text
"The professor was often late to class and seemed unprepared.
The lectures were confusing and the grading was inconsistent."
```

#### Mixed Review
```text
"The course material was interesting, but the professor's teaching style
didn't work for me. Office hours were helpful though."
```

## Features in Detail

### Sentiment Analysis
- Real-time processing of text input
- Breakdown into positive, negative, and neutral sentiments
- Confidence scores for each sentiment category

### Visualization
- Interactive donut chart
- Color-coded segments for each sentiment type
- Percentage indicators
- Clear labels with connecting lines
- Center total sentiment score

### Filtering Options
- Show All Sentiments
- Positive Only
- Negative Only
- Neutral Only

## Best Practices

1. **Input Quality**
- Write complete sentences
- Be specific in feedback
- Use clear language
- Include balanced feedback when applicable

2. **Performance Considerations**
- First analysis may have initial loading time
- Subsequent analyses are faster
- Model is cached for better performance
- Handles large text inputs efficiently

3. **Error Handling**
- Empty text validation
- Model loading error handling
- Invalid input type checking
- Clear error messages

## Browser Compatibility
- ✅ Chrome (recommended)
- ✅ Firefox
- ✅ Safari
- ✅ Edge

## Technical Notes
- Built with D3.js for visualizations
- Uses Hugging Face's transformers.js for sentiment analysis
- Requires ES6+ compatible browser
- Responsive design principles
- Real-time analysis capabilities
Insert cell
transformersJS = import('https://unpkg.com/@huggingface/transformers@3.4.2/dist/transformers.min.js?module')
Insert cell
import { ReactiveWidget } from "@john-guerra/reactive-widgets"
Insert cell
import {navio} from "@john-guerra/navio"
Insert cell
function SentimentWidget(
data,
{ value = null } = {}
) {
// Enhanced styling for the container
const element = html`
<div style="padding: 24px; border: 1px solid #e0e0e0; border-radius: 12px; max-width: 600px; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<textarea
placeholder="Enter professor review..."
style="width: 100%;
height: 100px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
resize: vertical;
margin-bottom: 16px;"
></textarea>
<div style="display: flex; gap: 12px; margin: 16px 0;">
<button style="
padding: 8px 16px;
background: #4a90e2;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
">Analyze</button>
<select style="
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
min-width: 120px;
cursor: pointer;
">
<option value="all">Show All</option>
<option value="POSITIVE">Positive Only</option>
<option value="NEGATIVE">Negative Only</option>
<option value="NEUTRAL">Neutral Only</option>
</select>
</div>
<div class="loading" style="
display: none;
color: #666;
font-style: italic;
margin: 10px 0;
">Analyzing...</div>
<div class="chart" style="
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
"></div>
</div>
`;

const textarea = element.querySelector("textarea");
const button = element.querySelector("button");
const chartDiv = element.querySelector(".chart");
const loading = element.querySelector(".loading");
const filterSelect = element.querySelector("select");

// Add hover effect to button
button.onmouseover = () => button.style.background = '#357abd';
button.onmouseout = () => button.style.background = '#4a90e2';

async function updateSentiment() {
const text = textarea.value;
if (!text.trim()) return;

loading.style.display = "block";
chartDiv.innerHTML = "";
try {
const result = await analyzeSentiment(text);
const predictions = result.flat();
const sentimentData = {
POSITIVE: 0,
NEGATIVE: 0,
NEUTRAL: 0
};
for (const s of predictions) {
sentimentData[s.label] = s.score;
}
const sum = sentimentData.POSITIVE + sentimentData.NEGATIVE;
sentimentData.NEUTRAL = Math.max(0, 1 - sum);
widget.value = sentimentData;
showValue();
} catch (e) {
chartDiv.innerHTML = `<div style="color: #d32f2f; padding: 16px; background: #ffebee; border-radius: 8px;">
Error: ${e.message}
</div>`;
} finally {
loading.style.display = "none";
}
}

function showValue() {
chartDiv.innerHTML = "";
if (!widget.value) return;

const selected = filterSelect.value;
const filtered = Object.entries(widget.value)
.filter(([label]) => selected === "all" || selected === label)
.map(([name, val]) => ({ name, value: val }));

chartDiv.appendChild(drawPieChart(filtered));
}

function drawPieChart(data) {
const width = 400, height = 400;
const radius = Math.min(width, height) / 2 * 0.65; // Reduced radius to give more space for labels
const labelOffset = radius * 1.2; // Increased offset for labels

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);

// Enhanced color scheme
const color = d3.scaleOrdinal()
.domain(data.map(d => d.name))
.range([
"#2ecc71", // Positive - green
"#f1c40f", // Neutral - yellow
"#e74c3c" // Negative - red
]);

const pie = d3.pie()
.value(d => d.value)
.sort(null);

const arc = d3.arc()
.innerRadius(radius * 0.6) // Increased inner radius for better donut look
.outerRadius(radius);

// Add slices
svg.selectAll("path")
.data(pie(data))
.join("path")
.attr("d", arc)
.attr("fill", d => color(d.data.name))
.attr("stroke", "white")
.style("stroke-width", "2px");

// Calculate label positions with anti-overlap
function calculateLabelPosition(d, i, data) {
const angle = (d.startAngle + d.endAngle) / 2;
const radians = angle - Math.PI / 2; // Rotate by -90 degrees
// Calculate base position
let x = Math.cos(radians) * labelOffset;
let y = Math.sin(radians) * labelOffset;

// Adjust vertical positions to prevent overlap
if (data.length > 1) {
const spacing = 25; // Minimum vertical spacing between labels
if (i > 0) {
// Move subsequent labels down
y += spacing * i;
}
}

return { x, y, angle: radians };
}

// Add labels with lines
const labelGroups = svg.selectAll(".label-group")
.data(pie(data))
.join("g")
.attr("class", "label-group");

// Add connecting lines
labelGroups.append("path")
.attr("class", "label-line")
.attr("stroke", "#999")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("d", (d, i) => {
const pos = calculateLabelPosition(d, i, data);
const midRadius = (arc.innerRadius()() + arc.outerRadius()()) / 2;
const midX = Math.cos(pos.angle) * midRadius;
const midY = Math.sin(pos.angle) * midRadius;
return `M ${midX},${midY} L ${pos.x},${pos.y}`;
});

// Add label backgrounds
labelGroups.append("rect")
.attr("transform", (d, i) => {
const pos = calculateLabelPosition(d, i, data);
return `translate(${pos.x - 45},${pos.y - 10})`;
})
.attr("width", 90)
.attr("height", 20)
.attr("fill", "white")
.attr("rx", 4)
.style("opacity", 0.9);

// Add label text
labelGroups.append("text")
.text(d => `${d.data.name}: ${(d.data.value * 100).toFixed(1)}%`)
.attr("transform", (d, i) => {
const pos = calculateLabelPosition(d, i, data);
return `translate(${pos.x},${pos.y})`;
})
.style("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "500")
.style("fill", "#333");

// Add center text for total
const total = d3.sum(data, d => d.value);
svg.append("text")
.text(`Total`)
.attr("text-anchor", "middle")
.attr("dy", "-0.5em")
.style("font-size", "14px")
.style("font-weight", "bold");

svg.append("text")
.text(`${(total * 100).toFixed(1)}%`)
.attr("text-anchor", "middle")
.attr("dy", "1em")
.style("font-size", "14px")
.style("font-weight", "bold");

return svg.node().ownerSVGElement;
}

button.onclick = updateSentiment;
filterSelect.oninput = showValue;

const widget = ReactiveWidget(element, { value, showValue });
showValue();
return widget;
}
Insert cell
huggingface_token = " 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"
Insert cell
async function analyzeSentiment(text) {
const response = await fetch(
"https://api-inference.huggingface.co/models/distilbert-base-uncased-finetuned-sst-2-english",
{
method: "POST",
headers: {
Authorization: `Bearer ${huggingface_token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ inputs: text })
}
);

if (!response.ok) {
throw new Error("Hugging Face API error: " + (await response.text()));
}

const result = await response.json();
return result;
}
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