zoomable sunburst on observable framework

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. :frowning:

---
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();

On this page you can see the canonical D3 example. Hope it helps.