Chunk Loading
Explore a large or remote history without fetching all of it first. Timescope requests time ranges at the data resolution needed by the view, so your endpoint can serve a detailed close-up or a summarized overview. See Core Concepts for the model, or try the Dynamic Loader example.
Pan to reveal new terrain, then zoom in to request finer detail. The terrain is synthetic height data generated locally by combining smooth variations at different scales; zooming in reveals finer detail. The vertical axis automatically fits the data. No network requests are made: Simulate loading delay adds a random wait of up to one second to each new terrain request. Uncheck it to load without the simulated wait. Cached data is reused either way. The lower Track shows the ranges returned by a companion loader without a delay, making chunk boundaries visible.
This is the terrain source used by the demo. terrain() generates a height at the given time and resolution. The loader checks the checkbox state, and the inline await simulates latency:
const delayCheckbox = document.querySelector('#example-simulate-loading-delay');
const source = createDataSource({
loader: async ({ range: [start, end], resolution }) => {
if (delayCheckbox.checked) {
await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
}
const rows = [];
// Align samples globally; include one neighbor on either side.
for (
let t = start.div(resolution).floor().sub(1).mul(resolution);
t.le(end.add(resolution));
t = t.add(resolution)
) {
rows.push({ time: t, value: terrain(t.number(), resolution.number()) });
}
return rows;
},
});Connect a range endpoint
For your own remote history, replace local generation with an endpoint. Include range and resolution placeholders in a source URL. Timescope substitutes them as the user pans and zooms:
<div id="chart" style="height: 240px"></div>import { Timescope } from 'timescope';
const timescope = new Timescope({
target: '#chart',
fit: [0, 3600],
sources: {
history: {
url: '/api/history?start={start}&end={end}&resolution={resolution}',
},
},
series: {
signal: { data: { source: 'history' }, chart: 'lines' },
},
tracks: { default: { timeAxis: { relative: true } } },
});Return a JSON array of rows such as { time: 15, value: 3 }, using the same row format as snapshot data. The endpoint chooses or computes data at the requested resolution; Timescope does not automatically summarize a range response for it.
A URL without placeholders loads a complete snapshot instead.
Use an application loader
Use a callback when requests need authentication, an SDK, or application-specific processing. Replace the URL source with a loader:
import { createDataSource } from 'timescope';
const history = createDataSource({
loader: async ({ range: [start, end], resolution }) => {
const query = new URLSearchParams({
start: start.toString(),
end: end.toString(),
resolution: resolution.toString(),
});
const response = await fetch(`/api/history?${query}`);
if (!response.ok) throw new Error(`History: ${response.status}`);
return response.json();
},
});Register this instance as sources: { history }. Function loaders use range mode by default; the request supplies Decimal values, so serializing with toString() retains their precision.
For a differently shaped payload, return row objects from the callback or use decoder / mappings. A URL decoder receives the fetched Response, while a callback decoder receives the callback's result.
Return rows for a range
Requests contain a finite range [start, end) and a positive resolution in the data's time units. The start is included and the end is excluded. Handle the requested boundaries even when they do not match a preferred chunk or the visible range exactly.
- Points: return points whose times fall inside the range.
- Intervals: return every overlapping interval, keeping its original start and end rather than trimming it to the request.
- Connections: include neighboring points on both sides where available: one for lines and steps, or two for curves. Include them even when the range itself has no points.
The neighboring points let Links continue across request boundaries. For bucketed data, keep bucket alignment consistent across requests and return whole buckets rather than clipping their rows. Built-in aggregate sources anchor buckets to chunkOrigin.
Match your endpoint's resolutions
If the service stores only certain sampling intervals, advertise them with source resolutions. For example, a service with one-second, one-minute, and one-hour data can use:
sources: {
history: {
url: '/api/history?start={start}&end={end}&resolution={resolution}',
resolutions: [1, 60, 3600],
},
}By default, a Series prefers the display resolution and snaps to the nearest available interval on the logarithmic zoom scale. Without source hints, it snaps to integer-zoom resolutions.
Use Series data.resolution to request a different density. For example, prefer two pixels per interval and round up to the next available interval:
data: {
source: 'history',
resolution: {
resolve: ({ resolution }) => resolution.mul(2),
snap: 'ceil',
},
}This changes data acquisition, not the visible time range. Resolution options describe fixed intervals, resolver callbacks, and snapping modes.
Tune request sizes
Preferred chunks span chunkSize × data resolution time units, aligned from chunkOrigin. The defaults are 256 and 0 respectively.
Use a larger chunkSize when your service benefits from larger batches, or a smaller one to reduce the amount acquired per chunk. A resolution-dependent size callback must return a stable size for each resolution. Loaders must still accept arbitrary requested boundaries.
cacheSize controls retention of inactive query results, and immediate controls loading while the view moves. See source options for their defaults and constraints.
For a history that also changes over time, see refreshing changed ranges.