Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
These demos showcase useStream client-side streaming of large datasets as coordinated chunks. The data is computed in the browser; the entire simplified chart paints first, then each chunk swaps serially to its full detail.
For the server-side equivalents — computing the chunks in RSC via abstractCreateStream — see the Abstract Create Stream benchmarks.
100,000 points streamed in chunks of 1,000. Each loading chunk simplifies its slice to 10 points; the coarse 1,000-point chart is up front, then each chunk swaps in its full 1,000-point detail serially.
'use client';
import * as React from 'react';
import { useStream } from '@mui/internal-docs-infra/useStream';
import type { StreamSource } from '@mui/internal-docs-infra/useStream';
import { CoordinatedLazy } from '@mui/internal-docs-infra/CoordinatedLazy';
const TOTAL = 100_000;
const CHUNK_SIZE = 1000;
const CHUNK_COUNT = TOTAL / CHUNK_SIZE; // 100
const SIMPLE_PER_CHUNK = 10;
const WIDTH = 900;
const HEIGHT = 220;
// A flowing multi-harmonic signal: a couple of low harmonics for the overall
// shape, plus finer detail whose amplitude swells and fades across the chart (the
// envelope) — so the 10-point loading slice reads as a clean sketch while the
// 1000-point detail adds the texture.
const TAU = Math.PI * 2;
const curve = (t: number) => {
const envelope = 0.4 + 0.6 * Math.abs(Math.sin(t * TAU * 1.3));
const base = 0.6 * Math.sin(t * TAU * 2.4) + 0.26 * Math.sin(t * TAU * 5.7 + 0.8);
const detail = envelope * (0.2 * Math.sin(t * TAU * 23) + 0.09 * Math.sin(t * TAU * 411));
return HEIGHT / 2 - ((HEIGHT / 2 - 16) / 1.2) * (base + detail);
};
const project = (globalIndex: number) =>
`${(globalIndex / TOTAL) * WIDTH},${curve(globalIndex / TOTAL)}`;
// Precompute every chunk's full and simplified polyline strings once (100k point
// projections at module load), so streaming and the serial swap stay cheap.
const FULL_PATHS: string[] = [];
const SIMPLE_PATHS: string[] = [];
for (let chunk = 0; chunk < CHUNK_COUNT; chunk += 1) {
const start = chunk * CHUNK_SIZE;
const full: string[] = [];
for (let offset = 0; offset <= CHUNK_SIZE; offset += 1) {
full.push(project(start + offset));
}
FULL_PATHS.push(full.join(' '));
const simple: string[] = [];
for (let step = 0; step < SIMPLE_PER_CHUNK; step += 1) {
simple.push(project(start + Math.round((step / (SIMPLE_PER_CHUNK - 1)) * CHUNK_SIZE)));
}
SIMPLE_PATHS.push(simple.join(' '));
}
interface Chunk {
index: number;
}
// Yield all 100 chunks at once, so the whole coarse chart is on screen before any
// detail swaps in.
const source: StreamSource<Chunk, void> = {
mode: 'stream',
async *stream(chunks, _options, signal) {
if (signal.aborted) {
return;
}
for (let index = 0; index < CHUNK_COUNT; index += 1) {
chunks.push({ index });
}
yield;
},
};
// Advances a front from 0 to `count`, one chunk per animation frame, so the detail
// swaps land serially as fast as the browser can paint them.
function useSerialFront(count: number): number {
const [front, setFront] = React.useState(0);
React.useEffect(() => {
let raf = 0;
let current = 0;
const tick = () => {
current += 1;
setFront(current);
if (current < count) {
raf = requestAnimationFrame(tick);
}
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [count]);
return front;
}
function Segment({ index, detailed }: { index: number; detailed: boolean }) {
return (
<CoordinatedLazy
ready={detailed}
fallback={
<polyline points={SIMPLE_PATHS[index]} fill="none" stroke="#cdbef0" strokeWidth={1} />
}
content={<polyline points={FULL_PATHS[index]} fill="none" stroke="#7c3aed" strokeWidth={1} />}
/>
);
}
export default function Page() {
const { chunks, Controller } = useStream<Chunk, void>({ source });
const front = useSerialFront(CHUNK_COUNT);
return (
<Controller>
<svg
width={WIDTH}
height={HEIGHT}
style={{ border: '1px solid #d0cdd7', borderRadius: 8, background: '#faf9fc' }}
>
{chunks.map((chunk) => (
// Detail sweeps right-to-left: the highest-index (rightmost) chunk swaps first.
<Segment
key={chunk.index}
index={chunk.index}
detailed={chunk.index >= CHUNK_COUNT - front}
/>
))}
</svg>
</Controller>
);
}50,000 points tiled into adaptive square chunks (greedy, left-to-right top-to-bottom: each row's first chunk fixes the row height, the rest grow their width to a target point count). Each loading chunk clusters its points into representative dots sized by density; the clustered chart paints first, then each chunk swaps in its full points serially.
'use client';
import * as React from 'react';
import { ScatterChart, generateScatterPoints } from './scatterChart';
const points = generateScatterPoints(50_000);
export default function Page() {
return (
<ScatterChart.Root points={points}>
<ScatterChart.Chunk>{(point) => <ScatterChart.Point point={point} />}</ScatterChart.Chunk>
</ScatterChart.Root>
);
}The same scatter at double the point count — heavier, for comparing how the streaming and final render scale with dataset size (up to 100k SVG circles, intentionally a stress test).
'use client';
import * as React from 'react';
import { ScatterChart, generateScatterPoints } from './scatterChart';
const points = generateScatterPoints(100_000);
export default function Page() {
return (
<ScatterChart.Root points={points}>
<ScatterChart.Chunk>{(point) => <ScatterChart.Point point={point} />}</ScatterChart.Chunk>
</ScatterChart.Root>
);
}