Skip to main content
This example demonstrates how to create animated data visualizations using popular charting libraries while maintaining full control over animation timing through Helios.

Overview

The data visualization examples show:
  • Disabling library animations for manual control
  • Frame-based data interpolation
  • Synchronized multi-series animations
  • Dynamic chart updates without flickering

Chart.js implementation

Complete example

composition.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chart.js Animation</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background: #ffffff;
            overflow: hidden;
        }
        #chart-container {
            width: 100vw;
            height: 100vh;
            position: relative;
        }
    </style>
</head>
<body>
    <div id="chart-container">
        <canvas id="chart"></canvas>
    </div>
    <script type="module" src="./src/main.ts"></script>
</body>
</html>
main.ts
import { Helios } from '@helios-project/core';
import Chart from 'chart.js/auto';

// Init Helios
const helios = new Helios({ fps: 30, duration: 5 });
helios.bindToDocumentTimeline();

// Expose for debugging
(window as any).helios = helios;

// Init Chart
const canvas = document.getElementById('chart') as HTMLCanvasElement;
const ctx = canvas.getContext('2d');

if (!ctx) throw new Error("Could not get canvas context");

const chart = new Chart(ctx, {
    type: 'bar',
    data: {
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
        datasets: [{
            label: 'Sales',
            data: [10, 20, 30, 40, 50, 60],
            backgroundColor: 'rgba(75, 192, 192, 0.6)',
            borderColor: 'rgba(75, 192, 192, 1)',
            borderWidth: 1
        }]
    },
    options: {
        animation: false, // Vital: Disable internal animation
        responsive: true,
        maintainAspectRatio: false,
        scales: {
            y: {
                beginAtZero: true,
                max: 100
            }
        }
    }
});

// Subscribe
helios.subscribe((state) => {
    const t = state.currentTime;

    // Animate data based on time
    const newData = chart.data.labels!.map((_, i) => {
        // Base value + oscillation
        // Offset phase by index i to create a wave
        return 50 + 40 * Math.sin(t * 2 + i * 0.5);
    });

    chart.data.datasets[0].data = newData;
    chart.update('none'); // Render immediately
});

Key patterns for Chart.js

Disable library animations

Always disable Chart.js internal animations to prevent conflicts:
options: {
    animation: false, // Critical for Helios control
    responsive: true,
    maintainAspectRatio: false
}

Update without animation

Use update('none') to render immediately without transitions:
chart.data.datasets[0].data = newData;
chart.update('none'); // No internal animation

Data interpolation

Create smooth transitions by calculating data values based on time:
const newData = chart.data.labels!.map((_, i) => {
    return 50 + 40 * Math.sin(t * 2 + i * 0.5);
});

D3.js implementation

Complete example

composition.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>D3 Animation</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      background-color: #f4f4f4;
      font-family: sans-serif;
    }
    #chart {
      width: 100vw;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }
  </style>
</head>
<body>
  <div id="chart"></div>
  <script type="module" src="./src/index.js"></script>
</body>
</html>
data.js
// Deterministic data series for the animation
export const DATA_SERIES = [
  // Year 2000
  [
    { name: "Alpha", value: 100, color: "#1f77b4" },
    { name: "Beta", value: 80, color: "#ff7f0e" },
    { name: "Gamma", value: 60, color: "#2ca02c" },
    { name: "Delta", value: 40, color: "#d62728" },
    { name: "Epsilon", value: 20, color: "#9467bd" }
  ],
  // Year 2001
  [
    { name: "Alpha", value: 120, color: "#1f77b4" },
    { name: "Beta", value: 90, color: "#ff7f0e" },
    { name: "Gamma", value: 110, color: "#2ca02c" },
    { name: "Delta", value: 50, color: "#d62728" },
    { name: "Epsilon", value: 30, color: "#9467bd" }
  ],
  // Additional years...
];
index.js
import { Helios } from '@helios-project/core';
import * as d3 from 'd3';
import { DATA_SERIES } from './data.js';

const duration = 5;
const helios = new Helios({ duration, fps: 30 });

// Expose for debugging/player
window.helios = helios;
helios.bindToDocumentTimeline();

// Setup SVG
const width = 800;
const height = 600;
const margin = { top: 30, right: 30, bottom: 30, left: 60 };

const svg = d3.select("#chart")
    .append("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("viewBox", [0, 0, width, height]);

// Scales
const x = d3.scaleBand()
    .range([margin.left, width - margin.right])
    .padding(0.1);

const y = d3.scaleLinear()
    .range([height - margin.bottom, margin.top]);

// Axes
const xAxis = g => g
    .attr("transform", `translate(0,${height - margin.bottom})`)
    .call(d3.axisBottom(x));

const yAxis = g => g
    .attr("transform", `translate(${margin.left},0)`)
    .call(d3.axisLeft(y));

// Initial render
const names = DATA_SERIES[0].map(d => d.name);
x.domain(names);
y.domain([0, 200]);

svg.append("g").call(xAxis);
const yAxisGroup = svg.append("g").call(yAxis);
const barGroup = svg.append("g");

// Update function
function update(time) {
    const totalIntervals = DATA_SERIES.length - 1;
    const intervalDuration = duration / totalIntervals;
    const clampedTime = Math.min(Math.max(time, 0), duration);

    // Find current interval
    let currentIntervalIndex = Math.floor(clampedTime / intervalDuration);
    if (currentIntervalIndex >= totalIntervals) {
        currentIntervalIndex = totalIntervals - 1;
    }

    const startData = DATA_SERIES[currentIntervalIndex];
    const endData = DATA_SERIES[currentIntervalIndex + 1];

    // Interpolation factor (0 to 1 within interval)
    let t = (clampedTime - (currentIntervalIndex * intervalDuration)) / intervalDuration;
    t = Math.max(0, Math.min(1, t));

    // Interpolate data
    const interpolatedData = startData.map(d => {
        const endD = endData ? endData.find(ed => ed.name === d.name) : d;
        return {
            name: d.name,
            value: d.value + (endD.value - d.value) * t,
            color: d.color
        };
    });

    // Update bars
    const bars = barGroup.selectAll("rect")
        .data(interpolatedData, d => d.name);

    bars.enter()
        .append("rect")
        .attr("fill", d => d.color)
        .attr("x", d => x(d.name))
        .attr("width", x.bandwidth())
        .merge(bars)
        .attr("y", d => y(d.value))
        .attr("height", d => y(0) - y(d.value));

    bars.exit().remove();
}

helios.subscribe(({ currentFrame, fps }) => {
    const time = currentFrame / fps;
    update(time);
});

Key patterns for D3.js

Interval-based interpolation

Interpolate between data snapshots using time-based calculations:
const totalIntervals = DATA_SERIES.length - 1;
const intervalDuration = duration / totalIntervals;
const currentIntervalIndex = Math.floor(clampedTime / intervalDuration);

const startData = DATA_SERIES[currentIntervalIndex];
const endData = DATA_SERIES[currentIntervalIndex + 1];

const t = (clampedTime - (currentIntervalIndex * intervalDuration)) / intervalDuration;

Value interpolation

Smooth transitions between data points:
const interpolatedData = startData.map(d => {
    const endD = endData.find(ed => ed.name === d.name);
    return {
        name: d.name,
        value: d.value + (endD.value - d.value) * t,
        color: d.color
    };
});

Immediate updates without transitions

Use the enter-merge-exit pattern without D3 transitions:
const bars = barGroup.selectAll("rect")
    .data(interpolatedData, d => d.name);

bars.enter()
    .append("rect")
    .merge(bars)
    .attr("y", d => y(d.value))
    .attr("height", d => y(0) - y(d.value));

Performance tips

Minimize DOM updates

Only update changed attributes:
// Good - only update changing values
bars.attr("y", d => y(d.value))
    .attr("height", d => y(0) - y(d.value));

// Avoid - updates all attributes every frame
bars.attr("x", d => x(d.name))
    .attr("y", d => y(d.value))
    .attr("width", x.bandwidth())
    .attr("height", d => y(0) - y(d.value));

Use fixed scales

Prevent scale recalculation by using fixed domains:
// Fixed domain (better performance)
y.domain([0, 200]);

// Dynamic domain (avoid if possible)
y.domain([0, d3.max(data, d => d.value)]).nice();

Cache D3 selections

Store selections to avoid repeated queries:
const barGroup = svg.append("g");
const bars = barGroup.selectAll("rect"); // Cache this

// Reuse in update function
function update(data) {
    bars.data(data).attr("height", d => y(d.value));
}

Batch data updates

Update all data at once instead of incrementally:
// Good - single update
chart.data.datasets[0].data = newData;
chart.update('none');

// Avoid - multiple updates
newData.forEach((value, index) => {
    chart.data.datasets[0].data[index] = value;
});
chart.update('none');

Advanced techniques

Multi-dataset animations

Animate multiple datasets with different timing:
helios.subscribe((state) => {
    const t = state.currentTime;
    
    // Dataset 1: Fast oscillation
    const data1 = labels.map((_, i) => 50 + 40 * Math.sin(t * 4 + i));
    
    // Dataset 2: Slow oscillation with offset
    const data2 = labels.map((_, i) => 30 + 20 * Math.sin(t * 1 + i + Math.PI));
    
    chart.data.datasets[0].data = data1;
    chart.data.datasets[1].data = data2;
    chart.update('none');
});

Custom easing functions

Implement custom interpolation for unique effects:
function easeInOutCubic(t) {
    return t < 0.5
        ? 4 * t * t * t
        : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

function interpolateWithEasing(start, end, t) {
    const easedT = easeInOutCubic(t);
    return start + (end - start) * easedT;
}

Race bar charts

Create racing bar chart animations with D3:
function update(time) {
    // Sort data by value
    const sortedData = [...interpolatedData].sort((a, b) => b.value - a.value);
    
    // Update scale domain with sorted names
    x.domain(sortedData.map(d => d.name));
    
    // Animate positions
    bars.data(sortedData, d => d.name)
        .attr("x", d => x(d.name))
        .attr("y", d => y(d.value));
}

Build docs developers (and LLMs) love