Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save rndmcnlly/9ed9ce4d42321866c5a6ec2ac811439d to your computer and use it in GitHub Desktop.
Save rndmcnlly/9ed9ce4d42321866c5a6ec2ac811439d to your computer and use it in GitHub Desktop.

USER

show me a directed graph layout with d3 in a self-ctoaned html exmaple

ASSISTANT

Here's a complete, self-contained HTML example that creates a directed graph using D3.js:

<!DOCTYPE html>
<html>
<head>
    <title>D3 Directed Graph</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        .links line {
            stroke: #999;
            stroke-opacity: 0.6;
            stroke-width: 2;
            marker-end: url(#arrowhead);
        }

        .nodes circle {
            stroke: #fff;
            stroke-width: 1.5px;
        }

        .node-label {
            font-family: Arial;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <svg width="600" height="400"></svg>

    <script>
        // Sample data
        const data = {
            nodes: [
                { id: 1, name: "Node 1" },
                { id: 2, name: "Node 2" },
                { id: 3, name: "Node 3" },
                { id: 4, name: "Node 4" },
                { id: 5, name: "Node 5" }
            ],
            links: [
                { source: 1, target: 2 },
                { source: 2, target: 3 },
                { source: 3, target: 4 },
                { source: 4, target: 5 },
                { source: 1, target: 5 },
                { source: 2, target: 4 }
            ]
        };

        // Set up the SVG
        const svg = d3.select("svg");
        const width = +svg.attr("width");
        const height = +svg.attr("height");

        // Define arrow marker
        svg.append("defs").append("marker")
            .attr("id", "arrowhead")
            .attr("viewBox", "0 -5 10 10")
            .attr("refX", 15)
            .attr("refY", 0)
            .attr("markerWidth", 8)
            .attr("markerHeight", 8)
            .attr("orient", "auto")
            .append("path")
            .attr("d", "M0,-5L10,0L0,5")
            .attr("fill", "#999");

        // Create the force simulation
        const simulation = d3.forceSimulation(data.nodes)
            .force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
            .force("charge", d3.forceManyBody().strength(-200))
            .force("center", d3.forceCenter(width / 2, height / 2));

        // Create the links
        const link = svg.append("g")
            .attr("class", "links")
            .selectAll("line")
            .data(data.links)
            .join("line");

        // Create the nodes
        const node = svg.append("g")
            .attr("class", "nodes")
            .selectAll("circle")
            .data(data.nodes)
            .join("circle")
            .attr("r", 10)
            .attr("fill", d => d3.schemeCategory10[d.id % 10]);

        // Add labels to nodes
        const labels = svg.append("g")
            .attr("class", "node-labels")
            .selectAll("text")
            .data(data.nodes)
            .join("text")
            .attr("class", "node-label")
            .text(d => d.name)
            .attr("dx", 12)
            .attr("dy", 4);

        // Add drag behavior
        node.call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));

        // Update positions on each tick
        simulation.on("tick", () => {
            link
                .attr("x1", d => d.source.x)
                .attr("y1", d => d.source.y)
                .attr("x2", d => d.target.x)
                .attr("y2", d => d.target.y);

            node
                .attr("cx", d => d.x)
                .attr("cy", d => d.y);

            labels
                .attr("x", d => d.x)
                .attr("y", d => d.y);
        });

        // Drag functions
        function dragstarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragended(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }
    </script>
</body>
</html>

This example creates a directed graph with the following features:

  1. Five nodes connected by directed edges
  2. Arrows showing the direction of relationships
  3. Draggable nodes
  4. Node labels
  5. Force-directed layout
  6. Colored nodes
  7. Smooth animations

The graph is interactive - you can:

  • Drag nodes around to rearrange the layout
  • The force simulation will automatically adjust other nodes and links
  • Nodes will return to a force-balanced position when released

The graph uses:

  • D3.js force simulation for layout
  • SVG for rendering
  • CSS for styling
  • Arrow markers for directed edges
  • Force-directed layout with link distance and charge forces

You can modify the data object to add more nodes and links or adjust the visual properties by modifying the CSS and D3 attributes.

To use this example, just save it as an HTML file and open it in a web browser. Make sure you have an internet connection as it loads D3.js from a CDN.

USER

Examine the structure of this file and then update the display to visualize how one scene links to another:

Title: UCSC Animal Crossing Start: Baskin Scenes: Baskin: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_0608.jpeg?v=1736887071471 Caption: It's a nice day. Maybe I should leave Baskin for a change. Choices: - Text: Walk towards McHenry Library Target: To_McHenry - Text: Walk towards Cowell Bus Stop Target: Cowell - Text: Walk towards East Field Target: Parking_Lot_Turkeys To_McHenry: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_3724.jpeg?v=1736886595142 Caption: I love the smell of books. Lets keep walking!
Choices: - Text: HUH? Whats that!? Target: Squirrel - Text: Too much nature... lets go back to Baskin Target: Baskin Squirrel: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_6398.jpeg?v=1736886665213 Caption: Hey I jus— SQUIRREL
Choices: - Text: Okay that was exciting, oh wait look down! Target: McHenry_Slug - Text: I've touched all the grass I can handle. Take me back the dungeon! Target: Baskin McHenry_Slug: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_3729.jpeg?v=1736886571773 Caption: I dare you to lick it.
Choices: - Text: I will lick... Target: El_Hospital - Text: No thank you<3 Target: McHenry McHenry: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_2034.JPG?v=1736888396630 Caption: You have arrived at McHenry! That was a close call. Choices: [] # McHenry ending El_Hospital: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/photo-1648224394432-8830fec15349.jpg?v=1736888206317 Caption: My bad sorry.
Choices: [] # Hospital ending Cowell: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_5625.jpeg?v=1736886699130 Caption: Looks like your bus isn't coming for a while... Choices: - Text: Who's crossing the street? Target: Cowell_Turkeys - Text: Walk towards East Field Target: East_Field Cowell_Turkeys: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_4840.jpeg?v=1736886678017 Caption: These new Loop buses look strange. I don't think they're stopping for me. Choices: - Text: Keep waiting for the bus, I have faith Target: Cowell - Text: I give up, I want to go back to Baskin Target: Baskin Parking_Lot_Turkeys: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/taturkeys.jpg?v=1736886773539 Caption: Weird-looking cars. They definitely don't understand right of way. Choices: - Text: Get away Target: East_Field East_Field: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_1463.jpg?v=1736888784697 Caption: It's such a beautiful day! For now... Choices: - Text: What's on the field? Target: Deer - Text: Keep walking along the path Target: Path Deer: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_0731.jpeg?v=1736886587410 Caption: You do belong here, little deer. Choices: - Text: Head back to the path Target: East_Field Path: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_9282.jpeg?v=1736886629411 Caption: I've got a long walk ahead of me. This 'touching grass' business is hard work. Choices: - Text: What's that on the pole? Target: Hawk - Text: Forge ahead Target: Cow Hawk: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_9280.jpeg?v=1736887026501 Caption: Hello there, hawk! Choices: - Text: I want to get a little closer... Target: Hawk_Close - Text: Let's retreat Target: Path Hawk_Close: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_6571.jpeg?v=1736886656612 Caption: Awesome... but maybe it doesn't want to be disturbed. Choices: - Text: Retreat Target: Path Cow: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_4760.jpeg?v=1736886595805 Caption: Whoa there! Who's this handsome devil? Choices: - Text: Pet the cow! Target: Cow_Pet - Text: Pet the cow! Target: Cow_Pet - Text: Pet the cow! Target: Cow_Pet Cow_Pet: ImageUrl: https://cdn.glitch.global/21220f1c-83fb-496f-8048-d11cdac5ba60/IMG_4758.jpeg?v=1736886564882 Caption: You feel fulfilled. Choices: [] # Cow ending

ASSISTANT

I'll help you create a directed graph visualization of this scene structure. Here's a modified version of the previous D3 graph that represents your game's scenes and connections:

<!DOCTYPE html>
<html>
<head>
    <title>UCSC Animal Crossing Scene Flow</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        .links line {
            stroke: #999;
            stroke-opacity: 0.6;
            stroke-width: 1.5;
            marker-end: url(#arrowhead);
        }

        .nodes circle {
            stroke: #fff;
            stroke-width: 1.5px;
        }

        .node-label {
            font-family: Arial;
            font-size: 10px;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <svg width="1200" height="800"></svg>

    <script>
        // Convert your scene data into nodes and links
        const scenes = {
            nodes: [
                { id: "Baskin", group: 1 },
                { id: "To_McHenry", group: 1 },
                { id: "Squirrel", group: 1 },
                { id: "McHenry_Slug", group: 1 },
                { id: "McHenry", group: 2 }, // ending
                { id: "El_Hospital", group: 2 }, // ending
                { id: "Cowell", group: 1 },
                { id: "Cowell_Turkeys", group: 1 },
                { id: "Parking_Lot_Turkeys", group: 1 },
                { id: "East_Field", group: 1 },
                { id: "Deer", group: 1 },
                { id: "Path", group: 1 },
                { id: "Hawk", group: 1 },
                { id: "Hawk_Close", group: 1 },
                { id: "Cow", group: 1 },
                { id: "Cow_Pet", group: 2 } // ending
            ],
            links: [
                { source: "Baskin", target: "To_McHenry" },
                { source: "Baskin", target: "Cowell" },
                { source: "Baskin", target: "Parking_Lot_Turkeys" },
                { source: "To_McHenry", target: "Squirrel" },
                { source: "To_McHenry", target: "Baskin" },
                { source: "Squirrel", target: "McHenry_Slug" },
                { source: "Squirrel", target: "Baskin" },
                { source: "McHenry_Slug", target: "El_Hospital" },
                { source: "McHenry_Slug", target: "McHenry" },
                { source: "Cowell", target: "Cowell_Turkeys" },
                { source: "Cowell", target: "East_Field" },
                { source: "Cowell_Turkeys", target: "Cowell" },
                { source: "Cowell_Turkeys", target: "Baskin" },
                { source: "Parking_Lot_Turkeys", target: "East_Field" },
                { source: "East_Field", target: "Deer" },
                { source: "East_Field", target: "Path" },
                { source: "Deer", target: "East_Field" },
                { source: "Path", target: "Hawk" },
                { source: "Path", target: "Cow" },
                { source: "Hawk", target: "Hawk_Close" },
                { source: "Hawk", target: "Path" },
                { source: "Hawk_Close", target: "Path" },
                { source: "Cow", target: "Cow_Pet" }
            ]
        };

        // Set up the SVG
        const svg = d3.select("svg");
        const width = +svg.attr("width");
        const height = +svg.attr("height");

        // Define arrow marker
        svg.append("defs").append("marker")
            .attr("id", "arrowhead")
            .attr("viewBox", "0 -5 10 10")
            .attr("refX", 20)
            .attr("refY", 0)
            .attr("markerWidth", 6)
            .attr("markerHeight", 6)
            .attr("orient", "auto")
            .append("path")
            .attr("d", "M0,-5L10,0L0,5")
            .attr("fill", "#999");

        // Create the force simulation
        const simulation = d3.forceSimulation(scenes.nodes)
            .force("link", d3.forceLink(scenes.links).id(d => d.id).distance(100))
            .force("charge", d3.forceManyBody().strength(-300))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("x", d3.forceX(width / 2).strength(0.1))
            .force("y", d3.forceY(height / 2).strength(0.1));

        // Create the links
        const link = svg.append("g")
            .attr("class", "links")
            .selectAll("line")
            .data(scenes.links)
            .join("line");

        // Create the nodes
        const node = svg.append("g")
            .attr("class", "nodes")
            .selectAll("circle")
            .data(scenes.nodes)
            .join("circle")
            .attr("r", d => d.group === 2 ? 12 : 8)
            .attr("fill", d => d.group === 2 ? "#ff7f0e" : "#1f77b4");

        // Add labels to nodes
        const labels = svg.append("g")
            .attr("class", "node-labels")
            .selectAll("text")
            .data(scenes.nodes)
            .join("text")
            .attr("class", "node-label")
            .text(d => d.id.replace(/_/g, " "))
            .attr("dx", 14)
            .attr("dy", 4);

        // Add drag behavior
        node.call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));

        // Update positions on each tick
        simulation.on("tick", () => {
            link
                .attr("x1", d => d.source.x)
                .attr("y1", d => d.source.y)
                .attr("x2", d => d.target.x)
                .attr("y2", d => d.target.y);

            node
                .attr("cx", d => d.x)
                .attr("cy", d => d.y);

            labels
                .attr("x", d => d.x)
                .attr("y", d => d.y);
        });

        // Drag functions
        function dragstarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragended(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }
    </script>
</body>
</html>

This visualization:

  1. Shows all scenes as nodes (circles)
  2. Connects scenes with directed arrows based on the choices
  3. Makes ending scenes (McHenry, El_Hospital, and Cow_Pet) larger and colored differently
  4. Allows dragging nodes to rearrange the layout
  5. Shows scene names as labels
  6. Uses force-directed layout to automatically position nodes
  7. Has arrows showing the direction of scene transitions

The ending scenes are marked in orange and are slightly larger than the regular scenes. You can drag nodes around to better see the connections, and the force simulation will help arrange them in a readable way.

USER

give me a variation that has a little textarea where i can paste in a fresh story in yaml syntax and your code visualizes without you needing to interpreted with your LLM mind or whatever

ASSISTANT

Here's a modified version that allows direct YAML input and visualization:

<!DOCTYPE html>
<html>
<head>
    <title>Story Graph Visualizer</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
    <style>
        .container {
            display: flex;
            height: 100vh;
        }
        .input-panel {
            width: 300px;
            padding: 10px;
            background: #f0f0f0;
        }
        .graph-panel {
            flex-grow: 1;
        }
        textarea {
            width: 100%;
            height: 300px;
        }
        .links line {
            stroke: #999;
            stroke-opacity: 0.6;
            stroke-width: 1.5;
            marker-end: url(#arrowhead);
        }
        .nodes circle {
            stroke: #fff;
            stroke-width: 1.5px;
        }
        .node-label {
            font-family: Arial;
            font-size: 10px;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="input-panel">
            <textarea id="yamlInput"></textarea>
            <button onclick="updateGraph()">Update Graph</button>
        </div>
        <div class="graph-panel">
            <svg width="900" height="800"></svg>
        </div>
    </div>

    <script>
        function parseStoryToGraph(story) {
            const nodes = [];
            const links = [];
            
            // Add start scene
            if (story.Start) {
                nodes.push({ id: story.Start, group: 1 });
            }

            // Process all scenes
            for (const [sceneId, scene] of Object.entries(story.Scenes)) {
                // Add node if not already added
                if (!nodes.find(n => n.id === sceneId)) {
                    // Check if it's an ending (no choices)
                    const isEnding = !scene.Choices || scene.Choices.length === 0;
                    nodes.push({ id: sceneId, group: isEnding ? 2 : 1 });
                }

                // Add links from choices
                if (scene.Choices) {
                    scene.Choices.forEach(choice => {
                        links.push({
                            source: sceneId,
                            target: choice.Target
                        });
                        
                        // Add target node if not already added
                        if (!nodes.find(n => n.id === choice.Target)) {
                            nodes.push({ id: choice.Target, group: 1 });
                        }
                    });
                }
            }

            return { nodes, links };
        }

        function createGraph(graphData) {
            // Clear existing SVG
            d3.select("svg").selectAll("*").remove();

            const svg = d3.select("svg");
            const width = +svg.attr("width");
            const height = +svg.attr("height");

            // Define arrow marker
            svg.append("defs").append("marker")
                .attr("id", "arrowhead")
                .attr("viewBox", "0 -5 10 10")
                .attr("refX", 20)
                .attr("refY", 0)
                .attr("markerWidth", 6)
                .attr("markerHeight", 6)
                .attr("orient", "auto")
                .append("path")
                .attr("d", "M0,-5L10,0L0,5")
                .attr("fill", "#999");

            const simulation = d3.forceSimulation(graphData.nodes)
                .force("link", d3.forceLink(graphData.links).id(d => d.id).distance(100))
                .force("charge", d3.forceManyBody().strength(-300))
                .force("center", d3.forceCenter(width / 2, height / 2))
                .force("x", d3.forceX(width / 2).strength(0.1))
                .force("y", d3.forceY(height / 2).strength(0.1));

            const link = svg.append("g")
                .attr("class", "links")
                .selectAll("line")
                .data(graphData.links)
                .join("line");

            const node = svg.append("g")
                .attr("class", "nodes")
                .selectAll("circle")
                .data(graphData.nodes)
                .join("circle")
                .attr("r", d => d.group === 2 ? 12 : 8)
                .attr("fill", d => d.group === 2 ? "#ff7f0e" : "#1f77b4");

            const labels = svg.append("g")
                .attr("class", "node-labels")
                .selectAll("text")
                .data(graphData.nodes)
                .join("text")
                .attr("class", "node-label")
                .text(d => d.id.replace(/_/g, " "))
                .attr("dx", 14)
                .attr("dy", 4);

            node.call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended));

            simulation.on("tick", () => {
                link
                    .attr("x1", d => d.source.x)
                    .attr("y1", d => d.source.y)
                    .attr("x2", d => d.target.x)
                    .attr("y2", d => d.target.y);

                node
                    .attr("cx", d => d.x)
                    .attr("cy", d => d.y);

                labels
                    .attr("x", d => d.x)
                    .attr("y", d => d.y);
            });

            function dragstarted(event, d) {
                if (!event.active) simulation.alphaTarget(0.3).restart();
                d.fx = d.x;
                d.fy = d.y;
            }

            function dragged(event, d) {
                d.fx = event.x;
                d.fy = event.y;
            }

            function dragended(event, d) {
                if (!event.active) simulation.alphaTarget(0);
                d.fx = null;
                d.fy = null;
            }
        }

        function updateGraph() {
            try {
                const yamlText = document.getElementById("yamlInput").value;
                const storyData = jsyaml.load(yamlText);
                const graphData = parseStoryToGraph(storyData);
                createGraph(graphData);
            } catch (e) {
                console.error("Error parsing YAML or creating graph:", e);
                alert("Error parsing YAML. Check console for details.");
            }
        }
    </script>
</body>
</html>

To use this:

  1. There's now a textarea on the left side where you can paste your YAML story structure
  2. Click the "Update Graph" button to visualize the story
  3. The graph will appear on the right side
  4. You can still drag nodes around to arrange them
  5. Ending scenes (those with no choices) are shown in orange and larger

The code will:

  • Parse the YAML input using js-yaml library
  • Convert the story structure into nodes and links
  • Create a directed graph visualization
  • Automatically detect ending scenes (scenes with no choices)
  • Show all connections between scenes

Just paste your YAML story structure into the textarea and click Update Graph to visualize it. If there are any errors in the YAML syntax, you'll get an alert and can check the console for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment