Skip to content

feat: 4 native Remotion templates — donut, hbars, line, radar (RFC-08 Phase 2)#37

Open
yasenchen-cmd wants to merge 1 commit into
nexu-io:mainfrom
yasenchen-cmd:feat/native-templates
Open

feat: 4 native Remotion templates — donut, hbars, line, radar (RFC-08 Phase 2)#37
yasenchen-cmd wants to merge 1 commit into
nexu-io:mainfrom
yasenchen-cmd:feat/native-templates

Conversation

@yasenchen-cmd

Copy link
Copy Markdown

Summary

Adds 4 native Remotion data-viz templates, completing the RFC-08 Phase 2 native template pattern established by frame-data-rollup. All adapted from Hermes Studio scene components.

Templates

Template Type Lines Source
frame-data-donut Ring/donut chart 382 SceneDonut
frame-data-hbars Horizontal bars 281 SceneDeptBars
frame-data-line Line chart 320 SceneMonthlyTrend
frame-data-radar Radar/spider 230 SceneRadar

Design decisions

  • Pure system fonts — no Google Fonts or external assets, deterministic offline rendering (avoids the all-black-frame trap that neutralizeBlockingResources() guards against in the bridge)
  • Responsive layout — every template uses useVideoConfig for canvas sizing; 16:9, 9:16, and 1:1 all work correctly
  • Spring physics — consistent animation language with frame-data-rollup (spring + interpolate, staggered per-item delays)
  • Log-scale fallback — hbars auto-switches to log scale when value spread exceeds 50×, keeping small bars legible
  • Provenance metadata — each template YAML records the Hermes Studio source via the three-layer provenance system (RFC-07)

Structure

Each template follows the established pattern:

templates/frame-{name}/
├── source/
│   ├── entry.ts           ← registerRoot
│   ├── Root.tsx           ← Composition with calculateMetadata
│   └── Data{Name}.tsx     ← Actual component
├── template.html-video.yaml  ← engine: remotion, native compositionId
├── SKILL.md
├── example.md
├── package.json
└── preview.png

Closes #--- (native template expansion)

… radar

Adapted from Hermes Studio (chenys/hermes-studio) scene components:

- frame-data-donut (DataDonut.tsx): SVG donut/ring chart with spring-animated
  slices, center figure rollup, staggered legend. 382 lines.
  ← src/video/scenes.tsx SceneDonut

- frame-data-hbars (DataHBars.tsx): Horizontal bars with top/bottom grouping,
  spring growth, rollup values, log-scale fallback. 281 lines.
  ← src/video/scenes.tsx SceneDeptBars

- frame-data-line (DataLine.tsx): SVG line chart with stroke-dashoffset path
  drawing, gradient area fill, staggered data points, trend arrow. 320 lines.
  ← src/video/scenes.tsx SceneMonthlyTrend

- frame-data-radar (DataRadar.tsx): SVG radar/spider chart with concentric
  grid rings, spring polygon, warn-threshold coloring. 230 lines.
  ← src/video/scenes.tsx SceneRadar

All templates: pure system fonts, responsive layout (16:9/9:16/1:1 via
useVideoConfig), no external assets, offline-deterministic.

Co-Authored-By: chenys <noreply@github.com>
@lefarcen lefarcen added size/XXL Size XXL (1500+ LOC) risk/medium Medium risk type/feature Feature change labels Jun 9, 2026
@lefarcen lefarcen requested a review from nettee June 9, 2026 01:28
@lefarcen

lefarcen commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Hey @yasenchen-cmd! 👋 Four data-viz templates in one shot — the spring-physics animation language consistency and the log-scale fallback on hbars are both thoughtful details that will matter a lot at render time. Nice to see Phase 2 land.

I've assigned @nettee for review and tagged the PR size/XXL / risk/medium / type/feature.

Since this is a large surface area, you might find it handy to have an AI agent drive the PR to merge hands-free once feedback arrives:

💡 To drive this PR to merge hands-free, paste this to your AI coding agent (Claude Code / Codex / opencode / Cursor …):
Take over nexu-io/html-video#37 until it merges — read https://raw.githubusercontent.com/nexu-io/looper/main/skills/pr-takeover/SKILL.md and follow it.

@nettee nettee left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found three blocking correctness issues in the new native templates. They are all in changed code/schema paths, and each one includes a concrete fix inline.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment on lines +145 to +158
const donutSize = Math.min(
width - padX * 2,
height - padY * 2 - (data.title ? height * 0.1 : 0),
);
if (!isVertical) {
// Leave room for legend on the right.
const donutMaxW = width * 0.55;
const legendW = width * 0.35;
const donutW = Math.min(donutMaxW, donutSize);
// donutSize stays as computed; the SVG will just be positioned left.
}
const donutR = Math.max(20, donutSize * 0.38);
const strokeW = Math.max(8, donutR * 0.26);
const cx = isVertical ? width / 2 : Math.round(width * 0.30);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wide-layout sizing here does not actually reserve space for the legend. On a supported 1:1 render (width=height=1080), donutSize becomes 907 while cx stays at width * 0.30 = 324, so the SVG is positioned at left = -129 and gets cropped off the left edge. The unused donutMaxW / donutW locals on lines 151-153 look like the intended fix. Please clamp the rendered donut size/position for non-vertical layouts, or switch 1:1 to the stacked layout, so every advertised aspect ratio stays fully on-canvas.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment on lines +89 to +94
const barHeight = Math.max(14, Math.round(height * 0.032));
const barGap = Math.round(barHeight * 0.65);
const labelW = Math.round(width * 0.2);
const barAreaX = padX + labelW + Math.round(width * 0.02);
const barAreaW = width - barAreaX - padX;
const chartH = items.length * (barHeight + barGap);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This layout cannot satisfy the schema's maxItems: 20. barHeight scales with canvas height and chartH = items.length * (barHeight + barGap), so at the default 1920x1080 size 20 rows need roughly 20 * (35 + 23) = 1160px before title/padding, which pushes the last bars below the frame; 9:16 is even worse. Because template.html-video.yaml accepts 20 items, callers can send valid input and get truncated output. Please either derive barHeight / barGap from the available vertical space or lower the schema cap to the largest count that fits every supported aspect ratio.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

const muted = props.muted ?? DEFAULTS.muted;
const accent = props.accent ?? DEFAULTS.accent;
const accentWarn = props.accentWarn ?? DEFAULTS.accentWarn;
const warnThreshold = props.warnThreshold ?? 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warn-threshold feature is wired to a different prop shape than the rest of this PR. Root.tsx, template.html-video.yaml, and example.md all pass warnThreshold inside data, but this component only reads props.warnThreshold, so the documented/sample input never highlights weak axes and the generated preview misses the advertised behavior. Please make the code, DataRadarData type, root sample, and YAML/example agree on one shape; the least disruptive fix is usually reading data.warnThreshold and adding that field to DataRadarData.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@lefarcen

lefarcen commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Hey @yasenchen-cmd@nettee's review is in with three correctness blockers. Each one has a concrete fix path right in the inline comments, so they should be straightforward to address:

  1. frame-data-donut — 1:1 render clips the SVG: at width=height=1080, the computed cx = width * 0.30 = 324 positions the donut off the left edge. The unused donutMaxW/donutW locals look like the intended guard — the fix is clamping position/size for non-vertical layouts, or routing 1:1 through the stacked path.

  2. frame-data-hbarsmaxItems: 20 overflows at 1920×1080: static barHeight means 20 rows need ~1160px before title/padding, which exceeds the canvas. Either derive bar height from the available vertical space or lower the schema cap to the largest count that fits across every supported aspect ratio.

  3. frame-data-radarwarnThreshold is silently disconnected: Root.tsx, template.html-video.yaml, and example.md all pass it inside data, but the component reads props.warnThreshold — so highlighting never fires on any sample input. The quickest fix is reading data.warnThreshold and adding it to DataRadarData.

Push fixes and re-request review from @nettee when ready. 🙌

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

Labels

risk/medium Medium risk size/XXL Size XXL (1500+ LOC) type/feature Feature change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants