Skip to main content
Glama
template.tsx.j2.bak10.5 kB
{/* chuk-mcp-remotion/src/chuk_mcp_remotion/components/charts/DonutChart/template.tsx.j2 */} import React from 'react'; import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from 'remotion'; interface DataPoint { label: string; value: number; color?: string; } interface DonutChartProps { data?: Array<{ label: string; value: number; color?: string }>; title?: string; centerText?: string; startFrame: number; durationInFrames: number; } export const DonutChart: React.FC<DonutChartProps> = ({ data = [], title, centerText, startFrame, durationInFrames }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const relativeFrame = frame - startFrame; // Don't render if outside the time range if (frame < startFrame || frame >= startFrame + durationInFrames) { return null; } if (data.length === 0) { return null; } // Calculate total const total = data.reduce((sum, item) => sum + item.value, 0); // Chart dimensions const chartSize = 500; const outerRadius = 200; const innerRadius = 120; const centerX = chartSize / 2; const centerY = chartSize / 2; // Entrance animation const entranceProgress = spring({ frame: relativeFrame, fps, config: { damping: 200, mass: 0.5, stiffness: 200 } }); // Exit animation const exitDuration = 20; const exitProgress = interpolate( relativeFrame, [durationInFrames - exitDuration, durationInFrames], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } ); const opacity = entranceProgress * exitProgress; const scale = interpolate(entranceProgress, [0, 1], [0.5, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); const titleTranslateY = interpolate(entranceProgress, [0, 1], [-30, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); // Default colors const defaultColors = [ '[[ colors.primary[0] ]]', '[[ colors.accent[0] ]]', '[[ colors.primary[1] ]]', '[[ colors.accent[1] ]]', '[[ colors.primary[2] ]]', '[[ colors.accent[2] ]]', '[[ colors.primary[3] ]]', '[[ colors.accent[3] ]]' ]; // Calculate slice paths const slices = []; let currentAngle = -90; // Start at top data.forEach((item, idx) => { const percentage = (item.value / total); const sliceAngle = percentage * 360; slices.push({ ...item, percentage, startAngle: currentAngle, endAngle: currentAngle + sliceAngle, color: item.color || defaultColors[idx % defaultColors.length] }); currentAngle += sliceAngle; }); // Function to create SVG donut arc path const createDonutPath = (startAngle: number, endAngle: number, outer: number, inner: number) => { const startRad = (startAngle * Math.PI) / 180; const endRad = (endAngle * Math.PI) / 180; const x1Outer = centerX + outer * Math.cos(startRad); const y1Outer = centerY + outer * Math.sin(startRad); const x2Outer = centerX + outer * Math.cos(endRad); const y2Outer = centerY + outer * Math.sin(endRad); const x1Inner = centerX + inner * Math.cos(startRad); const y1Inner = centerY + inner * Math.sin(startRad); const x2Inner = centerX + inner * Math.cos(endRad); const y2Inner = centerY + inner * Math.sin(endRad); const largeArc = endAngle - startAngle > 180 ? 1 : 0; return `M ${x1Outer} ${y1Outer} A ${outer} ${outer} 0 ${largeArc} 1 ${x2Outer} ${y2Outer} L ${x2Inner} ${y2Inner} A ${inner} ${inner} 0 ${largeArc} 0 ${x1Inner} ${y1Inner} Z`; }; return ( <AbsoluteFill style={{ pointerEvents: 'none' }}> <div style={{ position: 'absolute', top: '50%', left: '50%', transform: `translate(-50%, -50%) scale(${scale})`, opacity, fontFamily: "'[[ "', '".join(typography.primary_font.fonts) ]]'", display: 'flex', flexDirection: 'column', alignItems: 'center', gap: parseInt('[[ spacing.spacing['2xl'] ]]') }} > {title && ( <h3 style={{ fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution].lg ]]'), fontWeight: '[[ typography.font_weights.bold ]]', color: '[[ colors.text.on_dark ]]', textAlign: 'center', transform: `translateY(${titleTranslateY}px)`, textShadow: '0 [[ spacing.border_width.medium ]] [[ spacing.spacing.lg ]]px [[ colors.primary[0] ]]80', letterSpacing: '[[ typography.letter_spacing.tight ]]', margin: 0 }} > {title} </h3> )} <div style={{ display: 'flex', alignItems: 'center', gap: parseInt('[[ spacing.spacing['3xl'] ]]') }}> {/* Donut chart */} <div style={{ position: 'relative' }}> <svg width={chartSize} height={chartSize} style={{ filter: 'drop-shadow(0 10px 30px [[ colors.background.dark ]])' }} > <defs> <filter id="glowDonut"> <feGaussianBlur stdDeviation="2" result="coloredBlur"/> <feMerge> <feMergeNode in="coloredBlur"/> <feMergeNode in="SourceGraphic"/> </feMerge> </filter> </defs> {slices.map((slice, idx) => { const sliceDelay = idx * 3; const sliceProgress = spring({ frame: Math.max(0, relativeFrame - sliceDelay), fps, config: { damping: 150, mass: 0.5, stiffness: 200 } }); const animatedEndAngle = slice.startAngle + (slice.endAngle - slice.startAngle) * sliceProgress; const path = createDonutPath(slice.startAngle, animatedEndAngle, outerRadius, innerRadius); return ( <path key={idx} d={path} fill={slice.color} stroke="[[ colors.border.light ]]" strokeWidth="[[ spacing.border_width.medium ]]" filter="url(#glowDonut)" opacity={sliceProgress} /> ); })} </svg> {/* Center text */} {centerText && ( <div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', textAlign: 'center', opacity: interpolate(entranceProgress, [0, 1], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }) }} > <div style={{ fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution]['2xl'] ]]'), fontWeight: '[[ typography.font_weights.black ]]', color: '[[ colors.text.on_dark ]]', lineHeight: 1, textShadow: '0 [[ spacing.border_width.medium ]] 10px [[ colors.background.dark ]]' }} > {total} </div> <div style={{ fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution].xs ]]'), fontWeight: '[[ typography.font_weights.medium ]]', color: '[[ colors.text.muted ]]', marginTop: parseInt('[[ spacing.spacing.xs ]]') }} > {centerText} </div> </div> )} </div> {/* Legend */} <div style={{ display: 'flex', flexDirection: 'column', gap: parseInt('[[ spacing.spacing.sm ]]') }}> {slices.map((slice, idx) => { const legendDelay = idx * 3 + 10; const legendProgress = spring({ frame: Math.max(0, relativeFrame - legendDelay), fps, config: { damping: 150, mass: 0.5, stiffness: 200 } }); return ( <div key={idx} style={{ display: 'flex', alignItems: 'center', gap: parseInt('[[ spacing.spacing.sm ]]'), opacity: legendProgress, transform: `translateX(${interpolate(legendProgress, [0, 1], [parseInt('[[ spacing.spacing.lg ]]'), 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' })}px)` }} > <div style={{ width: parseInt('[[ spacing.spacing.lg ]]'), height: parseInt('[[ spacing.spacing.lg ]]'), borderRadius: parseInt('[[ spacing.border_radius.md ]]'), backgroundColor: slice.color, flexShrink: 0 }} /> <div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution].xs ]]'), fontWeight: '[[ typography.font_weights.semibold ]]', color: '[[ colors.text.on_dark ]]', lineHeight: '[[ typography.line_heights.tight ]]' }} > {slice.label} </div> <div style={{ fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution].xs ]]'), fontWeight: '[[ typography.font_weights.regular ]]', color: '[[ colors.text.muted ]]' }} > {slice.value} ({Math.round(slice.percentage * 100)}%) </div> </div> </div> ); })} </div> </div> </div> </AbsoluteFill> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-remotion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server