Skip to main content
Glama
template.tsx.j29.65 kB
{/* chuk-motion/src/chuk_motion/components/charts/PieChart/template.tsx.j2 */} import React from 'react'; import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from 'remotion'; interface DataPoint { label: string; value: number; color?: string; } interface PieChartProps { data?: Array<{ label: string; value: number; color?: string }>; title?: string; startFrame: number; durationInFrames: number; } export const PieChart: React.FC<PieChartProps> = ({ data = [], title, startFrame, durationInFrames }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const relativeFrame = frame - startFrame; // Don't render if outside the time range if (durationInFrames > 0 && (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 radius = 200; 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 from theme const defaultColors = [ '[[ colors.primary[0] ]]', '[[ colors.accent[0] ]]', '[[ colors.primary[1] if colors.primary|length > 1 else colors.accent[1] ]]', '[[ colors.accent[1] if colors.accent|length > 1 else colors.primary[0] ]]', '[[ colors.semantic.warning ]]', '[[ colors.semantic.success ]]', '[[ colors.semantic.error ]]', '[[ colors.primary[2] if colors.primary|length > 2 else colors.accent[0] ]]' ]; // 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 arc path const createArcPath = (startAngle: number, endAngle: number, r: number) => { const startRad = (startAngle * Math.PI) / 180; const endRad = (endAngle * Math.PI) / 180; const x1 = centerX + r * Math.cos(startRad); const y1 = centerY + r * Math.sin(startRad); const x2 = centerX + r * Math.cos(endRad); const y2 = centerY + r * Math.sin(endRad); const largeArc = endAngle - startAngle > 180 ? 1 : 0; return `M ${centerX} ${centerY} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} 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: '[[ 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 2px 20px [[ colors.primary[0] ]]80', letterSpacing: '[[ typography.letter_spacing.tight ]]', margin: 0 }} > {title} </h3> )} <div style={{ display: 'flex', alignItems: 'center', gap: parseInt('[[ spacing.spacing['3xl'] ]]') }}> {/* Pie chart */} <svg width={chartSize} height={chartSize} style={{ filter: `drop-shadow(0 10px 30px [[ colors.shadow.dark ]])` }} > <defs> <filter id="glowPie"> <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 = createArcPath(slice.startAngle, animatedEndAngle, radius); // Label position const midAngle = (slice.startAngle + animatedEndAngle) / 2; const labelRadius = radius * 0.7; const labelX = centerX + labelRadius * Math.cos(midAngle * Math.PI / 180); const labelY = centerY + labelRadius * Math.sin(midAngle * Math.PI / 180); return ( <g key={idx}> <path d={path} fill={slice.color} stroke="[[ colors.border.light ]]" strokeWidth="[[ spacing.border_width.medium ]]" filter="url(#glowPie)" opacity={sliceProgress} /> {sliceProgress > 0.5 && ( <text x={labelX} y={labelY} textAnchor="middle" dominantBaseline="middle" fill="[[ colors.text.on_dark ]]" fontSize="[[ typography.font_sizes[typography.default_resolution].xs ]]" fontWeight="[[ typography.font_weights.bold ]]" fontFamily="'[[ "', '".join(typography.primary_font.fonts) ]]'" opacity={interpolate(sliceProgress, [0.5, 1], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' })} > {Math.round(slice.percentage * 100)}% </text> )} </g> ); })} </svg> {/* 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: '[[ 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: '[[ 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