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
{/* chuk-motion/src/chuk_motion/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 (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 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>
);
};