Documentation Index
Fetch the complete documentation index at: https://mintlify.com/BintzGavin/helios/llms.txt
Use this file to discover all available pages before exploring further.
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
<!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>
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
<!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>
// 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...
];
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
};
});
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));
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));
}