Skip to main content

Line Chart

line-chart.tsx
import {loadFont} from '@remotion/google-fonts/Inter'; import React from 'react'; import {Easing, Interactive, interpolate, useCurrentFrame} from 'remotion'; const {fontFamily} = loadFont('normal', { subsets: ['latin'], weights: ['700', '800'], }); const data = [ {label: 'Mar', value: 24}, {label: 'Apr', value: 32}, {label: 'May', value: 29}, {label: 'Jun', value: 45}, {label: 'Jul', value: 51}, {label: 'Aug', value: 63}, {label: 'Sep', value: 74}, ]; const CHART_WIDTH = 1400; const CHART_HEIGHT = 520; const CHART_SIDE_PADDING = 16; const MIN_VALUE = 20; const MAX_VALUE = 80; const Y_AXIS_VALUES = [80, 60, 40, 20]; const springEasing = Easing.spring({ damping: 14.5, mass: 0.8, overshootClamping: false, stiffness: 100, }); export const LineChart: React.FC = () => { const frame = useCurrentFrame(); const latestValueProgress = frame >= 70 ? 1 : interpolate(frame, [58, 70], [0, 1], { easing: springEasing, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const points = data.map(({label, value}, index) => ({ label, value, x: CHART_SIDE_PADDING + (index / (data.length - 1)) * (CHART_WIDTH - CHART_SIDE_PADDING * 2), y: ((MAX_VALUE - value) / (MAX_VALUE - MIN_VALUE)) * CHART_HEIGHT, })); const linePath = points.reduce((path, {x, y}, index) => { if (index === 0) { return `M ${x} ${y}`; } return `${path} L ${x} ${y}`; }, ''); const firstPoint = points[0]; const latestPoint = points[points.length - 1]; const areaPath = `${linePath} L ${latestPoint.x} ${CHART_HEIGHT} L ${firstPoint.x} ${CHART_HEIGHT} Z`; return ( <Interactive.Div name="Chart" style={{ color: '#111827', display: 'flex', flexDirection: 'column', fontFamily, fontVariantNumeric: 'tabular-nums', gap: 84, height: '100%', justifyContent: 'center', translate: '30px 0px', }} > <Interactive.H1 name="Title" style={{ color: '#111827', fontSize: 76, fontWeight: 800, letterSpacing: -3.8, lineHeight: 0.95, margin: 0, translate: '0 -32px', }} > Monthly active users </Interactive.H1> <Interactive.Div name="Plot area" style={{ height: 520, marginBottom: 64, position: 'relative', }} > <div style={{ color: '#4b5563', fontSize: 40, fontWeight: 700, height: '100%', position: 'absolute', right: `calc(${100 - (CHART_SIDE_PADDING / CHART_WIDTH) * 100}% + 64px)`, top: 0, width: 94, }} > {Y_AXIS_VALUES.map((value) => ( <Interactive.Div key={value} name="Y-axis label" style={{ position: 'absolute', right: 0, textAlign: 'right', top: `${((MAX_VALUE - value) / (MAX_VALUE - MIN_VALUE)) * 100}%`, translate: '0 -50%', whiteSpace: 'nowrap', width: '100%', }} > {value}K </Interactive.Div> ))} </div> <svg viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`} preserveAspectRatio="none" style={{ height: '100%', overflow: 'visible', position: 'absolute', width: '100%', }} > <Interactive.G name="Grid lines" fill="none" stroke="#d1d5db" strokeWidth={2} > {Y_AXIS_VALUES.map((value) => { const y = ((MAX_VALUE - value) / (MAX_VALUE - MIN_VALUE)) * CHART_HEIGHT; return ( <line key={value} x1={CHART_SIDE_PADDING} x2={CHART_WIDTH - CHART_SIDE_PADDING} y1={y} y2={y} /> ); })} </Interactive.G> <Interactive.Path name="Area fill" d={areaPath} fill="#2858e8" style={{ opacity: interpolate(frame, [48, 68], [0, 0.1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }), }} /> <Interactive.Path name="Trend line" d={linePath} fill="none" pathLength={1} stroke="#2858e8" strokeDasharray="1 1" strokeDashoffset={interpolate(frame, [14, 58], [1, 0], { easing: Easing.bezier(0, 0, 0.58, 1), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', })} strokeLinecap="round" strokeLinejoin="round" strokeWidth={12} /> <Interactive.G name="Data points" fill="#ffffff" stroke="#2858e8" strokeWidth={8} > {points.map(({label, x, y}, index) => ( <circle key={label} cx={x} cy={y} r={interpolate( frame, [ index === 0 ? 7 : 14 + index * 7, index === 0 ? 15 : 22 + index * 7, ], [0, 11], { easing: Easing.bezier(0.34, 1.56, 0.64, 1), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }, )} /> ))} </Interactive.G> </svg> <div style={{ color: '#4b5563', fontSize: 40, fontWeight: 700, height: 48, left: 0, position: 'absolute', right: 0, top: 'calc(100% + 64px)', }} > {data.map(({label}, index) => index % 2 === 0 ? ( <Interactive.Div key={label} name="X-axis label" style={{ left: `${((CHART_SIDE_PADDING + (index / (data.length - 1)) * (CHART_WIDTH - CHART_SIDE_PADDING * 2)) / CHART_WIDTH) * 100}%`, position: 'absolute', top: 0, translate: '-50% 0', }} > {label} </Interactive.Div> ) : null, )} </div> <div style={{ left: `${(latestPoint.x / CHART_WIDTH) * 100}%`, position: 'absolute', top: `${(latestPoint.y / CHART_HEIGHT) * 100}%`, translate: '-50% -140%', }} > <Interactive.Div name="Latest value" style={{ backgroundColor: '#2858e8', borderRadius: 12, color: '#ffffff', fontSize: 44, fontWeight: 800, letterSpacing: -1.5, lineHeight: 1, padding: '18px 24px', scale: `${latestValueProgress}`, transformOrigin: 'bottom center', visibility: frame <= 58 ? 'hidden' : 'visible', whiteSpace: 'nowrap', willChange: 'transform', }} > {latestPoint.value}K </Interactive.Div> </div> </Interactive.Div> </Interactive.Div> ); };