Hello!
I am trying to recreate a zoomable sunburst on observable framework for a uni project. I would say I am half way there, as the graph shows up and when I hover on top of a slice I can read both the sentiment and the specific text objects in the category. This being said, I am facing a list of errors that I am unable to fix:
- labels do not show up.
- the graph is not interactive.
- the width of the graph is as wide as the page permits, not the present.
The D3 part is in the last tab. I added the others for context. Please hep.
---
theme: dashboard
toc: false
---
# Topics copy
This is the topics view.
```js
//IMPORT VEGA LITE
import * as vega from "npm:vega";
import * as vegaLite from "npm:vega-lite";
import * as vegaLiteApi from "npm:vega-lite-api";
const vl = vegaLiteApi.register(vega, vegaLite);
//for file attachment
import {FileAttachment} from "npm:@observablehq/stdlib";
//for using require in imports
import {require} from "npm:d3-require";
//import for nlp
const nlp = require('compromise');
//TENSORFLOW.js import for sentiment analysis
const tf = require("@tensorflow/tfjs@latest");
//FOR D3 GRAPHS
import * as d3 from "npm:d3";
//LOADING TOPICS FILE
var topics = FileAttachment("your_topics.json").json();
//sentiment analysis metadata and model
const metadata = [];
const model = [];
let loadedResources = null;
async function loadResources() {
if (!loadedResources) {
const modelUrl = "https://storage.googleapis.com/tfjs-models/tfjs/sentiment_cnn_v1/model.json";
const metadataUrl = "https://storage.googleapis.com/tfjs-models/tfjs/sentiment_cnn_v1/metadata.json";
const [model, metadataResponse] = await Promise.all([
tf.loadLayersModel(modelUrl),
fetch(metadataUrl)
]);
const metadata = await metadataResponse.json();
loadedResources = { model, metadata };
}
return loadedResources;
}
// SENTIMENT ANALYSIS
async function predictSentiment(text) {
const { model, metadata } = await loadResources();
const trimmed = text.trim().toLowerCase().replace(/(\.|\,|\!)/g, '').split(' ');
const inputBuffer = tf.buffer([1, metadata.max_len], "float32");
trimmed.forEach((word, i) => {
const index = metadata.word_index[word] || 0;
inputBuffer.set(index + metadata.index_from, 0, i);
});
const input = inputBuffer.toTensor();
const predictOut = await model.predict(input);
const positivity = predictOut.dataSync()[0];
predictOut.dispose();
return positivity;
}
// Function to determine mood from positivity
function getMood(positivity) {
if (positivity > 0.9) return '😍';
else if (positivity > 0.8) return '😀';
else if (positivity > 0.6) return '🙂';
else if (positivity > 0.4) return '😐';
else if (positivity > 0.2) return '🙁';
else if (positivity > 0.1) return '😦';
else if (positivity > 0) return '😱';
else return '🤔';
}
// Main function to analyze sentiment and display result
async function analyzeSentiment(text) {
try {
const positivity = await predictSentiment(text);
return getMood(positivity);
} catch (error) {
console.error("Error in sentiment analysis:", error);
return `ERROR: ${error.message}`;
}
}
// TEST - to delete later
async function displayTextMood(text) {
try {
let textMood = await analyzeSentiment(text); // Await the promise to get the actual mood
display(`The mood associated with ${text} is: ${textMood}`);
} catch (error) {
console.error("Error fetching sentiment:", error);
}
}
// Example usage
displayTextMood("Bread & butter");
// Create a function to asynchronously gather sentiment data
async function createSentimentDictionary(topics) {
let sentimentPromises = topics.topics_your_topics.map(topic => {
let i_text = topic.string_map_data.Name.value;
return analyzeSentiment(i_text).then(sentiment => ({
text: i_text,
sentiment: sentiment
}));
});
return await Promise.all(sentimentPromises);
}
let globalMoodMapping = {};
async function processTopics() {
try {
const sentimentDict = await createSentimentDictionary(await topics);
const moodMapping = {
'😍': [], '😀': [], '🙂': [], '😐': [],
'🙁': [], '😦': [], '😱': [], '🤔': []
};
sentimentDict.forEach(item => {
if (moodMapping.hasOwnProperty(item.sentiment)) {
moodMapping[item.sentiment].push(item.text);
}
});
globalMoodMapping = {
name: "Sentiments",
children: Object.keys(moodMapping).map(mood => ({
name: mood,
children: moodMapping[mood].map(text => ({ name: text, value: 1 }))
}))
};
} catch (error) {
console.error("Error processing topics:", error);
}
}
// DISPLAY SENTIMENT AS ZOOMABLE SUNBURST USING D3
function createPartitionChart(data) {
const width = 500;
const height = width;
const radius = width / 2;
const color = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, data.children.length + 1));
const hierarchy = d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
const partition = d3.partition()
.size([2 * Math.PI, radius]);
const root = partition(hierarchy);
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, width])
.style("font", "10px sans-serif");
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(0.005)
.padRadius(radius / 3)
.innerRadius(d => d.y0)
.outerRadius(d => d.y1 - 1);
svg.append("g")
.selectAll("path")
.data(root.descendants().filter(d => d.depth))
.enter().append("path")
.attr("fill", d => { while (d.depth > 1) d = d.parent; return color(d.data.name); })
.attr("d", arc)
.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${d.value}`);
function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
svg.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().filter(d => d.depth && labelVisible(d)))
.enter().append("text")
.attr("transform", d => labelTransform(d))
.text(d => d.data.name)
.style("fill-opacity", d => +labelVisible(d))
.style("display", d => labelVisible(d) ? null : "none");
return svg.node();
}
// Display moods function integrates all parts to show the final visualization
async function displayMoods() {
await processTopics(); // Ensure the global variable is updated first
console.log("Accessing Global Mood Mapping:", globalMoodMapping);
const data = globalMoodMapping;
const svgChart = createPartitionChart(data);
document.body.appendChild(svgChart); // Or use any other method to attach the SVG to the DOM
}
displayMoods();