PTENES
MODULE 4.3

📈 Data Story

Data alone is boring. Data told as a story is memorable. This module puts together a video that guides viewers through a narrative built on numbers: animated charts, counters, comparisons, and highlighted text — orchestrated with Series and transitions cross-dissolve.

6
Topics
45
Minutes
Advanced
Level
Project
Type
data JSON/props chart-animation donut-chart stat-counter comparison-chart Data-Story narrative consistent with the data
1

📖 Narrative structure — the data arc

An effective Data-Story doesn’t start with the chart — it starts with the problem. The viewer needs to understand what's at stake before seeing the number. The sequence of scenes automatically creates this narrative arc when you order the Series well.

📈 The 5-scene arc

1.Context — one sentence that defines the problem. Ex: "In 2025, 68% of videos were watched without sound."
2.Central data point — the most important number, animated with a large stat-counter.
3.Proportion — donut chart showing how this number is divided.
4.Trend — line chart showing change over time.
5.Conclusion — highlighted text with the core message + call to action.

💡 The 5–8-second-per-scene rule

Each data scene should last between 5 and 8 seconds. Less than 5 s: the viewer can't process the data. More than 8 s: the pacing slows and it feels like a PowerPoint presentation. Cross-dissolve transitions of 10–15 frames cover the change without interrupting the flow.

5 scenes
Full arc
5–8 s
By data scene
Context
Always first
Total
~30–45 s
2

📊 Animated bar chart — chart-animation

O chart-animation.tsx uses an array of values and animates each bar growing from 0 to its proportional height. The stagger between bars makes the chart look like it’s “filling up” — the viewer’s eye follows the tallest bar, which is usually the most important data point.

Bars with stagger chart-animation.tsx
const data = [42, 68, 55, 91, 37]; // valores %
const maxH = 200;        // px de altura máxima

{data.map((val, i) => {
  const delay = i * 6;  // stagger de 6 frames
  const barHeight = interpolate(
    frame, [delay, delay + 45],
    [0, (val / 100) * maxH],
    { extrapolateRight: "clamp", easing: Easing.out(Easing.quad) }
  );
  // rótulo aparece quando barra chega ao topo
  const labelOpacity = interpolate(
    frame, [delay + 40, delay + 55], [0, 1],
    { extrapolateRight: "clamp" }
  );
  return <div key={i} style={{ height: `${barHeight}px`, opacity: 1 }}>
    <span style={{ opacity: labelOpacity }}>{val}%</span>
  </div>;
})}

✓ Bars that work

  • ✓Stagger of 4–8 frames between bars — noticeable without feeling drawn out.
  • ✓The label appears when the bar finishes — 15 frames after the bar delay.
  • ✓Easing.out for growth — starts fast and slows down as it reaches the top.

✗ Chart mistakes

  • ✗All bars growing at the same time—loses the reveal effect.
  • ✗The label appears before the bar finishes — it looks like a bug.
  • ✗Easing.in for growth — starts too slowly and feels stuck.
Stagger
delay = i * 6
Height
(val/100) * maxH
Easing
out(quad)
Label
+15f after bar
3

🍩 Proportion donut chart

O donut-chart.tsx uses SVG with stroke-dasharray e stroke-dashoffset animated. Each slice is a <circle> with arc length calculated from the percentage. The progressive reveal follows the same logic as the bars: Easing.out, revealing each slice with a small stagger.

1

Calculate circumference and offsets

const c = 2 * Math.PI * r — circumference of the circle. For a 42% slice, the arc length is c * 0.42. O strokeDasharray é "arco restante" and the offset is rotated to position it correctly.

2

strokeDashoffset animation

Starts with dashoffset = c (invisible slice) and animates up to dashoffset = c - arco (complete slice). The result is the slice being "drawn" clockwise.

3

Caption appears after reveal

Each donut color has a legend that fades in when its corresponding slice finishes appearing. The legend and slice stay visually connected.

strokeDasharray
arc + remainder
strokeDashoffset
c → c-arco
Rotation
-90° to start at the top
Caption
fade after reveal
4

🔢 stat-counter and text-highlight combined

The central data scene combines two templates: stat-counter.tsx with the number animating from 0 to the value, and text-highlight.tsx that highlights a contextual phrase with a colored background growing from 0% to 100% width. The two enter in a stagger — first the number, then the text.

Counter + highlight in sequence DataScene.tsx
const frame = useCurrentFrame();

// número conta de 0 → 68% em 60 frames
const pct = Math.round(interpolate(
  frame, [0, 60], [0, 68],
  { easing: Easing.out(Easing.cubic), extrapolateRight: "clamp" }
));

// highlight cresce de 0 → 100% a partir do frame 50
const hlWidth = interpolate(
  frame, [50, 80], [0, 100],
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);

return <AbsoluteFill style={{ display:"flex", flexDirection:"column", alignItems:"center", justifyContent:"center" }}>
  <span style={{ fontSize: 120, fontWeight: 800, color: "#fbbf24" }}>{pct}%</span>
  <div style={{ position: "relative" }}>
    <div style={{ position:"absolute", inset:0, background:"rgba(251,191,36,0.25)", width:`${hlWidth}%` }}/>
    <span>dos vídeos são assistidos sem som</span>
  </div>
</AbsoluteFill>;
Counter
0 → value in 60f
Highlight
width 0 → 100%
Stagger
Counter first
Font
120px+ for impact
5

⚔️ Comparison chart — comparison-chart

O comparison-chart.tsx shows two side-by-side bars that grow at different rates — the most powerful visual argument for showing a difference. The golden rule: the “bad” bar appears first, grows a little, and then the “good” bar dramatically overtakes it.

✓ Impactful comparison

  • ✓Negative bar (red) appears first — low anchor.
  • ✓Positive bar (amber/green) grows afterward and overtakes it — visual drama.
  • ✓Percentage difference label appears with a spring animation above the taller bar.

✗ Weak comparison

  • ✗Both bars growing together—loses the “overtaking” effect.
  • ✗Similar colors on both sides — the comparison isn’t obvious.
  • ✗Without a difference label, the viewer has to calculate it mentally.
Order
Bad → good
Delay
+15f for the 2nd bar
Colors
Red vs. amber
Δ%
Spring at the top
6

🔀 Cross-dissolve transitions between scenes

Cross-dissolve is the ideal transition for Data-Story: fluid enough not to disrupt the train of thought, while clearly marking the scene change. It is implemented by overlapping two Sequence components and animating their opacities in opposite directions during the overlapping frames.

Manual cross-dissolve with overlap DataStory.tsx
// Cena A: frames 0–90 | Cena B: frames 78–168 | overlap: 78–90
const DISSOLVE = 12; // frames de sobreposição

return (
  <AbsoluteFill>
    <Sequence from={0} durationInFrames={90}>
      <div style={{ opacity: interpolate(frame,
        [90 - DISSOLVE, 90], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }) }}>
        <CenaA />
      </div>
    </Sequence>
    <Sequence from={90 - DISSOLVE}>
      <div style={{ opacity: interpolate(frame,
        [90 - DISSOLVE, 90], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }) }}>
        <CenaB />
      </div>
    </Sequence>
  </AbsoluteFill>
);

💡 10–15 frames of dissolve

A 10-frame dissolve (0.33 s at 30fps) is almost imperceptible but softens the cut. A 15-frame dissolve (0.5 s) is noticeable and more elegant. Above 20 frames, it starts to feel intentionally dramatic — use it only if you want that effect.

Overlap
10–15 frames
Scene A
opacity 1 → 0
Scene B
opacity 0 → 1
extrapolate
clamp on both

🏁 Module Summary

✓
5-scene arc — context → data → proportion → trend → conclusion.
✓
Bars with stagger — delay = i * 6, label +15f after bar, Easing.out(quad).
✓
Donut via SVG — strokeDashoffset animated from c to c-arc with -90° rotation.
✓
Counter + highlight — large number with animated width beneath the contextual text.
✓
Dramatic comparison — bad bar first, good one overtakes it with a 15-frame delay.
✓
Cross-dissolve — 12 frames of overlap, with inverse opacities between consecutive scenes.

Next Module:

4.4 — Reels / Shorts: 9:16 vertical video, typewriter, glitch, sound-wave, bokeh, and subscribe reminder.