Created
March 17, 2026 03:10
-
-
Save jpatel3/bc248a4c03e0074680c8efb87e6c4857 to your computer and use it in GitHub Desktop.
Data-Driven Animations — Scaling Plan for Tuva Jr.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Data-Driven Animations — Scaling Plan | Tuva</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --tuva-green: #27ae60; | |
| --tuva-green-light: #e8f8ef; | |
| --tuva-green-dark: #1e8c4c; | |
| --text: #1a1a2e; | |
| --text-secondary: #4a4a6a; | |
| --border: #e0e0e8; | |
| --bg: #ffffff; | |
| --bg-alt: #f7f8fa; | |
| --code-bg: #f0f2f5; | |
| --shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); | |
| } | |
| html { font-size: 16px; scroll-behavior: smooth; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| color: var(--text); | |
| background: var(--bg-alt); | |
| line-height: 1.7; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| /* Header */ | |
| .site-header { | |
| background: var(--bg); | |
| border-bottom: 3px solid var(--tuva-green); | |
| padding: 1.5rem 2rem; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| box-shadow: var(--shadow); | |
| } | |
| .site-header-inner { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| text-decoration: none; | |
| } | |
| .brand-logo { | |
| width: 36px; height: 36px; | |
| background: var(--tuva-green); | |
| border-radius: 8px; | |
| display: flex; align-items: center; justify-content: center; | |
| color: white; font-weight: 700; font-size: 1.1rem; | |
| } | |
| .brand-text { font-size: 1.1rem; font-weight: 600; color: var(--text); } | |
| .brand-text span { color: var(--text-secondary); font-weight: 400; } | |
| .header-date { font-size: 0.85rem; color: var(--text-secondary); } | |
| /* Main container */ | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 2rem 2rem 4rem; | |
| } | |
| /* Title block */ | |
| .title-block { | |
| background: var(--bg); | |
| border-radius: 12px; | |
| padding: 2.5rem 2.5rem 2rem; | |
| margin-bottom: 2rem; | |
| box-shadow: var(--shadow); | |
| border-left: 5px solid var(--tuva-green); | |
| } | |
| .title-block h1 { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| color: var(--text); | |
| line-height: 1.3; | |
| margin-bottom: 0.5rem; | |
| } | |
| .title-block .subtitle { | |
| font-size: 1.05rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0; | |
| } | |
| /* Table of contents */ | |
| .toc { | |
| background: var(--bg); | |
| border-radius: 12px; | |
| padding: 1.75rem 2rem; | |
| margin-bottom: 2rem; | |
| box-shadow: var(--shadow); | |
| } | |
| .toc h2 { | |
| font-size: 0.85rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--tuva-green); | |
| margin-bottom: 1rem; | |
| font-weight: 700; | |
| } | |
| .toc ol { | |
| list-style: none; | |
| counter-reset: toc-counter; | |
| padding: 0; | |
| } | |
| .toc ol li { | |
| counter-increment: toc-counter; | |
| margin-bottom: 0.4rem; | |
| } | |
| .toc ol li::before { | |
| content: counter(toc-counter) "."; | |
| color: var(--tuva-green); | |
| font-weight: 600; | |
| margin-right: 0.5rem; | |
| font-size: 0.9rem; | |
| } | |
| .toc a { | |
| color: var(--text); | |
| text-decoration: none; | |
| font-size: 0.95rem; | |
| transition: color 0.15s; | |
| } | |
| .toc a:hover { color: var(--tuva-green); } | |
| /* Sections */ | |
| .section { | |
| background: var(--bg); | |
| border-radius: 12px; | |
| padding: 2rem 2.5rem; | |
| margin-bottom: 1.5rem; | |
| box-shadow: var(--shadow); | |
| } | |
| h2 { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--text); | |
| margin-bottom: 1.25rem; | |
| padding-bottom: 0.6rem; | |
| border-bottom: 2px solid var(--tuva-green-light); | |
| } | |
| h2 .section-num { | |
| color: var(--tuva-green); | |
| margin-right: 0.25rem; | |
| } | |
| h3 { | |
| font-size: 1.15rem; | |
| font-weight: 600; | |
| color: var(--text); | |
| margin-top: 1.5rem; | |
| margin-bottom: 0.75rem; | |
| } | |
| h3:first-child { margin-top: 0; } | |
| h4 { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| margin-top: 1.25rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| p { margin-bottom: 1rem; } | |
| /* Lists */ | |
| ul, ol { | |
| margin-bottom: 1rem; | |
| padding-left: 1.5rem; | |
| } | |
| li { margin-bottom: 0.35rem; } | |
| li strong { color: var(--text); } | |
| /* Callout / highlight boxes */ | |
| .callout { | |
| background: var(--tuva-green-light); | |
| border-left: 4px solid var(--tuva-green); | |
| border-radius: 0 8px 8px 0; | |
| padding: 1rem 1.25rem; | |
| margin: 1.25rem 0; | |
| font-size: 0.95rem; | |
| } | |
| .callout p:last-child { margin-bottom: 0; } | |
| .callout-neutral { | |
| background: var(--bg-alt); | |
| border-left: 4px solid var(--border); | |
| border-radius: 0 8px 8px 0; | |
| padding: 1rem 1.25rem; | |
| margin: 1.25rem 0; | |
| font-size: 0.95rem; | |
| } | |
| /* Option cards */ | |
| .option-card { | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 1.5rem 1.75rem; | |
| margin: 1.25rem 0; | |
| position: relative; | |
| } | |
| .option-card.recommended { | |
| border-color: var(--tuva-green); | |
| border-width: 2px; | |
| } | |
| .option-card.recommended::after { | |
| content: "Recommended"; | |
| position: absolute; | |
| top: -0.65rem; | |
| right: 1.25rem; | |
| background: var(--tuva-green); | |
| color: white; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| padding: 0.15rem 0.65rem; | |
| border-radius: 4px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.03em; | |
| } | |
| .pros-cons { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 1rem; | |
| margin-top: 1rem; | |
| } | |
| .pros, .cons { | |
| padding: 0.75rem 1rem; | |
| border-radius: 8px; | |
| font-size: 0.9rem; | |
| } | |
| .pros { background: #e8f8ef; } | |
| .cons { background: #fdf0ed; } | |
| .pros strong { color: #1e8c4c; } | |
| .cons strong { color: #c0392b; } | |
| .pros ul, .cons ul { padding-left: 1.25rem; margin-bottom: 0; } | |
| .pros li, .cons li { margin-bottom: 0.2rem; font-size: 0.88rem; } | |
| /* Tables */ | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 1rem 0 1.25rem; | |
| font-size: 0.92rem; | |
| } | |
| thead th { | |
| background: var(--tuva-green); | |
| color: white; | |
| font-weight: 600; | |
| text-align: left; | |
| padding: 0.65rem 0.9rem; | |
| font-size: 0.85rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.03em; | |
| } | |
| thead th:first-child { border-radius: 8px 0 0 0; } | |
| thead th:last-child { border-radius: 0 8px 0 0; } | |
| tbody td { | |
| padding: 0.6rem 0.9rem; | |
| border-bottom: 1px solid var(--border); | |
| vertical-align: top; | |
| } | |
| tbody tr:nth-child(even) { background: var(--bg-alt); } | |
| tbody tr:last-child td:first-child { border-radius: 0 0 0 8px; } | |
| tbody tr:last-child td:last-child { border-radius: 0 0 8px 0; } | |
| /* Code blocks */ | |
| code { | |
| font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace; | |
| font-size: 0.88em; | |
| background: var(--code-bg); | |
| padding: 0.15em 0.4em; | |
| border-radius: 4px; | |
| color: #c0392b; | |
| } | |
| pre { | |
| background: #1a1a2e; | |
| color: #e8e8f0; | |
| padding: 1.25rem 1.5rem; | |
| border-radius: 10px; | |
| overflow-x: auto; | |
| margin: 1rem 0 1.25rem; | |
| font-size: 0.85rem; | |
| line-height: 1.6; | |
| } | |
| pre code { | |
| background: none; | |
| padding: 0; | |
| color: inherit; | |
| font-size: inherit; | |
| } | |
| /* JSON syntax hints */ | |
| .json-key { color: #82aaff; } | |
| .json-str { color: #c3e88d; } | |
| .json-comment { color: #6a6a8a; } | |
| /* Workflow steps */ | |
| .workflow-steps { | |
| counter-reset: step-counter; | |
| list-style: none; | |
| padding-left: 0; | |
| } | |
| .workflow-steps li { | |
| counter-increment: step-counter; | |
| position: relative; | |
| padding-left: 2.75rem; | |
| margin-bottom: 0.75rem; | |
| } | |
| .workflow-steps li::before { | |
| content: counter(step-counter); | |
| position: absolute; | |
| left: 0; top: 0.1rem; | |
| width: 1.8rem; height: 1.8rem; | |
| background: var(--tuva-green); | |
| color: white; | |
| border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 0.8rem; font-weight: 700; | |
| } | |
| /* Effort badge */ | |
| .effort-badge { | |
| display: inline-block; | |
| background: var(--tuva-green-light); | |
| color: var(--tuva-green-dark); | |
| font-size: 0.82rem; | |
| font-weight: 600; | |
| padding: 0.2rem 0.7rem; | |
| border-radius: 20px; | |
| margin-left: 0.5rem; | |
| } | |
| /* Phase timeline */ | |
| .phase-timeline { | |
| position: relative; | |
| padding-left: 2rem; | |
| margin: 1.5rem 0; | |
| } | |
| .phase-timeline::before { | |
| content: ""; | |
| position: absolute; | |
| left: 0.55rem; | |
| top: 0.5rem; | |
| bottom: 0.5rem; | |
| width: 3px; | |
| background: linear-gradient(to bottom, var(--tuva-green), var(--tuva-green-light)); | |
| border-radius: 2px; | |
| } | |
| .phase-item { | |
| position: relative; | |
| margin-bottom: 1.5rem; | |
| padding: 1rem 1.25rem; | |
| background: var(--bg-alt); | |
| border-radius: 10px; | |
| border: 1px solid var(--border); | |
| } | |
| .phase-item::before { | |
| content: ""; | |
| position: absolute; | |
| left: -1.7rem; top: 1.25rem; | |
| width: 12px; height: 12px; | |
| background: var(--tuva-green); | |
| border: 3px solid white; | |
| border-radius: 50%; | |
| box-shadow: 0 0 0 2px var(--tuva-green); | |
| } | |
| .phase-item h4 { | |
| margin-top: 0; | |
| color: var(--tuva-green-dark); | |
| font-size: 1rem; | |
| } | |
| .phase-item p:last-child { margin-bottom: 0; } | |
| .phase-time { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.04em; | |
| } | |
| /* Request card */ | |
| .request-card { | |
| background: var(--tuva-green-light); | |
| border-radius: 12px; | |
| padding: 1.5rem 1.75rem; | |
| margin: 1rem 0; | |
| } | |
| .request-card ol { margin-bottom: 0; } | |
| .request-card li { margin-bottom: 0.6rem; } | |
| /* File tree */ | |
| .file-tree { | |
| font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace; | |
| font-size: 0.85rem; | |
| background: var(--bg-alt); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 1.25rem 1.5rem; | |
| margin: 1rem 0; | |
| line-height: 1.8; | |
| } | |
| .file-tree .dir { color: var(--tuva-green-dark); font-weight: 600; } | |
| .file-tree .file { color: var(--text-secondary); } | |
| .file-tree .desc { color: #8888a8; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 0.82rem; } | |
| /* Separator */ | |
| hr { | |
| border: none; | |
| height: 1px; | |
| background: var(--border); | |
| margin: 1.5rem 0; | |
| } | |
| /* Footer */ | |
| .footer { | |
| text-align: center; | |
| color: var(--text-secondary); | |
| font-size: 0.85rem; | |
| padding: 2rem 0 1rem; | |
| font-style: italic; | |
| } | |
| /* Print styles */ | |
| @media print { | |
| body { background: white; } | |
| .site-header { position: static; border-bottom-width: 2px; box-shadow: none; } | |
| .section, .title-block, .toc { box-shadow: none; break-inside: avoid; } | |
| .container { padding: 0; } | |
| .option-card { break-inside: avoid; } | |
| pre { white-space: pre-wrap; word-wrap: break-word; } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 700px) { | |
| .container { padding: 1rem; } | |
| .section, .title-block, .toc { padding: 1.25rem 1.5rem; } | |
| .pros-cons { grid-template-columns: 1fr; } | |
| .site-header { padding: 1rem; } | |
| .title-block h1 { font-size: 1.5rem; } | |
| table { font-size: 0.82rem; } | |
| thead th, tbody td { padding: 0.4rem 0.6rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header class="site-header"> | |
| <div class="site-header-inner"> | |
| <div class="brand"> | |
| <div class="brand-logo">T</div> | |
| <div class="brand-text">Tuva <span>Engineering</span></div> | |
| </div> | |
| <div class="header-date">March 2026</div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="container"> | |
| <!-- Title --> | |
| <div class="title-block"> | |
| <h1>Data-Driven Animations</h1> | |
| <p class="subtitle">Scaling Plan — from POC to Content Team Self-Service</p> | |
| </div> | |
| <!-- Table of Contents --> | |
| <nav class="toc"> | |
| <h2>Contents</h2> | |
| <ol> | |
| <li><a href="#poc">What We Built (POC)</a></li> | |
| <li><a href="#options">How to Scale: Three Options</a></li> | |
| <li><a href="#roadmap">Recommended Roadmap</a></li> | |
| <li><a href="#requests">Content Team: How to Request an Animation</a></li> | |
| <li><a href="#technical">Technical Details (for Developers)</a></li> | |
| </ol> | |
| </nav> | |
| <!-- Section 1: What We Built --> | |
| <section class="section" id="poc"> | |
| <h2><span class="section-num">1.</span> What We Built (POC)</h2> | |
| <p>A system in <code>tuva-tools-viewer</code> where students click a data point in Tuva Data Tools and see a real-world animation of what that data represents. Two working demos:</p> | |
| <ul> | |
| <li><strong>Seasonal Shadow Length</strong> — Sun moves across the sky at the correct angle, casting the correct shadow length. Students see how sun angle and shadow length are connected in real life.</li> | |
| <li><strong>Wolves Hunting Bison</strong> — Wolf pack 🐺 surrounds a bison 🦬. Pack size matches the data. Success rate bar shows hunting effectiveness. Fun verdict text changes (“Strength in numbers!”, “Hunting is hard work!”).</li> | |
| </ul> | |
| <div class="callout"> | |
| <p>Both use real data from tuvalabs.com, embedded in the app with pre-configured Tuva Jr. plot states.</p> | |
| </div> | |
| <h3>Architecture</h3> | |
| <ul> | |
| <li>Each animation is a <strong>template</strong>: a dataset + a React SVG scene component</li> | |
| <li>Templates live in <code>client/src/components/data-animations/templates/</code></li> | |
| <li>Adding a new one: create a folder, add <code>Template.ts</code> + <code>Scene.tsx</code>, register in <code>index.ts</code></li> | |
| <li>Scene components receive the selected case’s data as props and render SVG</li> | |
| </ul> | |
| </section> | |
| <!-- Section 2: Three Options --> | |
| <section class="section" id="options"> | |
| <h2><span class="section-num">2.</span> How to Scale: Three Options</h2> | |
| <!-- Option A --> | |
| <div class="option-card recommended"> | |
| <h3>Option A: LLM-Powered Generation <span class="effort-badge">30-60 min / animation</span></h3> | |
| <p>Content team writes a natural language prompt. Claude generates the scene component. A developer reviews and integrates.</p> | |
| <h4>Workflow</h4> | |
| <ol class="workflow-steps"> | |
| <li>Content person identifies a dataset and writes a description: <em>“Dataset about roller coaster physics with columns speed_mph, height_feet, g_force. Show a roller coaster car on a track. Height controls position on the hill, speed shows motion lines, g_force shows rider leaning back.”</em></li> | |
| <li>Developer feeds this to Claude along with the <code>AnimationTemplate</code> interface and 2 existing examples</li> | |
| <li>Claude generates the Scene component (React + SVG + emoji)</li> | |
| <li>Developer reviews, tweaks, drops into <code>templates/</code> folder</li> | |
| <li>Add one line to <code>index.ts</code> → deployed</li> | |
| </ol> | |
| <div class="pros-cons"> | |
| <div class="pros"> | |
| <strong>Pros</strong> | |
| <ul> | |
| <li>Works today with no new tooling</li> | |
| <li>High visual quality — LLMs are surprisingly good at SVG</li> | |
| <li>Flexible — any visual concept</li> | |
| <li>Content team drives the creative vision</li> | |
| </ul> | |
| </div> | |
| <div class="cons"> | |
| <strong>Cons</strong> | |
| <ul> | |
| <li>Still needs a developer to review/integrate</li> | |
| <li>Each animation is custom code</li> | |
| <li>Quality varies, may need iteration</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Option B --> | |
| <div class="option-card"> | |
| <h3>Option B: Scene Template Library + Config <span class="effort-badge">5-15 min / animation</span></h3> | |
| <p>Build a library of reusable scene types configurable via JSON. Content team picks a type and maps columns.</p> | |
| <h4>Pre-built Scene Types to Consider</h4> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Scene Type</th> | |
| <th>Visual</th> | |
| <th>Example Use</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><code>sky-angle</code></td> | |
| <td>Sun/moon on arc + shadow</td> | |
| <td>Shadow length, solar energy</td> | |
| </tr> | |
| <tr> | |
| <td><code>count-vs-target</code></td> | |
| <td>N entities surrounding target</td> | |
| <td>Wolves/bison, predator/prey</td> | |
| </tr> | |
| <tr> | |
| <td><code>scale-comparison</code></td> | |
| <td>Object grows/shrinks with data</td> | |
| <td>Plant growth, population</td> | |
| </tr> | |
| <tr> | |
| <td><code>bar-race</code></td> | |
| <td>Animated bars with icons</td> | |
| <td>Comparing categories</td> | |
| </tr> | |
| <tr> | |
| <td><code>timeline</code></td> | |
| <td>Object moves along a path</td> | |
| <td>Migration, seasons</td> | |
| </tr> | |
| <tr> | |
| <td><code>before-after</code></td> | |
| <td>Split scene, two states</td> | |
| <td>Treatment effects</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <h4>Example Configuration</h4> | |
| <pre><code>{ | |
| <span class="json-key">"sceneType"</span>: <span class="json-str">"count-vs-target"</span>, | |
| <span class="json-key">"datasetSlug"</span>: <span class="json-str">"wolves_hunting_bison"</span>, | |
| <span class="json-key">"entityEmoji"</span>: <span class="json-str">"🐺"</span>, | |
| <span class="json-key">"targetEmoji"</span>: <span class="json-str">"🦬"</span>, | |
| <span class="json-key">"countColumn"</span>: <span class="json-str">"wolf-hunting-group-size-attrib0"</span>, | |
| <span class="json-key">"successColumn"</span>: <span class="json-str">"successful-captures-attrib2"</span>, | |
| <span class="json-key">"background"</span>: <span class="json-str">"yellowstone"</span> | |
| }</code></pre> | |
| <div class="pros-cons"> | |
| <div class="pros"> | |
| <strong>Pros</strong> | |
| <ul> | |
| <li>No developer needed</li> | |
| <li>Consistent quality</li> | |
| <li>Fast</li> | |
| </ul> | |
| </div> | |
| <div class="cons"> | |
| <strong>Cons</strong> | |
| <ul> | |
| <li>Limited to pre-built types</li> | |
| <li>Upfront investment (~1-2 weeks per scene type)</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Option C --> | |
| <div class="option-card"> | |
| <h3>Option C: Visual Editor <span class="effort-badge">2-3 months to build</span></h3> | |
| <p>Web-based drag-and-drop editor. Canvas with elements, property bindings (“when column X changes, move this element”), preview with real data.</p> | |
| <div class="pros-cons"> | |
| <div class="pros"> | |
| <strong>Pros</strong> | |
| <ul> | |
| <li>Full self-serve</li> | |
| <li>Unlimited creativity</li> | |
| </ul> | |
| </div> | |
| <div class="cons"> | |
| <strong>Cons</strong> | |
| <ul> | |
| <li>Major engineering investment</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Section 3: Roadmap --> | |
| <section class="section" id="roadmap"> | |
| <h2><span class="section-num">3.</span> Recommended Roadmap</h2> | |
| <div class="phase-timeline"> | |
| <div class="phase-item"> | |
| <span class="phase-time">Now → 1 month</span> | |
| <h4>Phase 1: LLM Generation</h4> | |
| <ul> | |
| <li>Use Option A to build 5–10 more animations</li> | |
| <li>Validate which datasets benefit most</li> | |
| <li>Identify recurring visual patterns</li> | |
| <li>Content team submits requests via a simple form/doc: dataset URL, column names, visual concept sketch</li> | |
| </ul> | |
| </div> | |
| <div class="phase-item"> | |
| <span class="phase-time">1 – 3 months</span> | |
| <h4>Phase 2: Template Library</h4> | |
| <ul> | |
| <li>Review the 10+ animations built in Phase 1</li> | |
| <li>Extract 3–4 recurring scene types as configurable templates (Option B)</li> | |
| <li>Content team self-serves for datasets matching those patterns</li> | |
| <li>LLM generation continues for unique/custom scenes</li> | |
| </ul> | |
| </div> | |
| <div class="phase-item"> | |
| <span class="phase-time">3 – 6 months (if demand warrants)</span> | |
| <h4>Phase 3: Config UI</h4> | |
| <ul> | |
| <li>Build a simple web form (not full editor): select scene type, pick columns, choose emoji/colors, preview</li> | |
| <li>Fraction of the cost of a full visual editor</li> | |
| <li>Content team fully independent for supported scene types</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Section 4: How to Request --> | |
| <section class="section" id="requests"> | |
| <h2><span class="section-num">4.</span> Content Team: How to Request an Animation</h2> | |
| <p>For Phase 1, submit requests with:</p> | |
| <div class="request-card"> | |
| <ol> | |
| <li><strong>Dataset URL</strong> on tuvalabs.com</li> | |
| <li><strong>Which columns</strong> should drive the animation (and what they represent)</li> | |
| <li><strong>Visual concept</strong> — 2–3 sentences describing what you want students to see. Be specific: <em>“Show a penguin standing on an iceberg. As temperature increases, the iceberg shrinks. The penguin looks worried when temperature > 5°C.”</em></li> | |
| <li><strong>Key insight</strong> — what should students understand? <em>“Higher temperatures = smaller icebergs = less penguin habitat”</em></li> | |
| </ol> | |
| </div> | |
| <div class="callout"> | |
| <p>The more specific the visual description, the better the generated animation will be.</p> | |
| </div> | |
| </section> | |
| <!-- Section 5: Technical Details --> | |
| <section class="section" id="technical"> | |
| <h2><span class="section-num">5.</span> Technical Details <span style="font-weight: 400; font-size: 0.85em; color: var(--text-secondary);">(for developers)</span></h2> | |
| <p><strong>Repository:</strong> <code>tuva-tools-viewer</code> → <code>client/src/components/data-animations/</code></p> | |
| <h3>Key Files</h3> | |
| <div class="file-tree"> | |
| <span class="dir">data-animations/</span><br> | |
| <span class="dir">templates/</span><br> | |
| <span class="file">types.ts</span> <span class="desc">— AnimationTemplate and SceneProps interfaces</span><br> | |
| <span class="file">index.ts</span> <span class="desc">— Template registry (add new templates here)</span><br> | |
| <span class="dir">shadow-length/</span> <span class="desc">— Reference for sky/angle scenes</span><br> | |
| <span class="dir">wolves-hunting/</span> <span class="desc">— Reference for count/entity scenes</span><br> | |
| <span class="file">useDataToolsSelection.ts</span> <span class="desc">— Hook that polls Tuva Data Tools for selected case</span><br> | |
| <span class="dir">pages/</span><br> | |
| <span class="file">DataAnimationsPage.tsx</span> <span class="desc">— Landing page</span><br> | |
| <span class="file">DataAnimationWorkspacePage.tsx</span> <span class="desc">— 2-panel workspace</span><br> | |
| </div> | |
| <h3>Data Integration</h3> | |
| <p>Datasets are embedded as TypeScript constants (fetched from <code>GET /api/datasets/<slug></code> with auth). Plot states from the API are included for pre-configured scatter plots.</p> | |
| <h3>Selection Detection</h3> | |
| <p>Polls <code>toolRef.current.actions.getPlotState()</code> every 200ms, reads <code>plotview.dataSet.selectedCases</code> array. The DynG mediator is internal to the UMD bundle and not accessible from the parent app.</p> | |
| </section> | |
| <!-- Footer --> | |
| <div class="footer"> | |
| Generated from the Data-Driven Animations POC — Tuva Jr. · March 2026 | |
| </div> | |
| </div> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment