Paragon

Waiting for several asynchronous things

A report is captured with a screenshot, not converted. By default the engine prints the page once it has loaded, the network is idle and fonts are ready. Anything that draws after that — a chart, a map, a table built from a second fetch — is not on the page yet when the picture is taken.

The template says when it is really finished. For one piece of drawing, paragon.ready() is enough. For several, use the barrier: register each piece, and the engine waits for the last one.

Nothing needs to be loaded or imported. window.paragon is injected into every template before your scripts run.

This object used to be called reportEngine, and that name still resolves to the same object, so a template written before the rename keeps working untouched. Write new templates against paragon.


The API

Call Use it for
paragon.waitFor(promise) Anything that gives you a promise. Returns the promise, so it chains
paragon.pending() Callback-style code with no promise. Returns a release function to call when that piece is done
paragon.ready() Opens the gate immediately, whatever else is outstanding

The page is captured once every registered piece has finished. ready() is the escape hatch, not the normal path — with a barrier in play it cuts off work that is still running.

Set Template draws asynchronously on the report ("awaitsReadySignal": true in the manifest) whenever this code lives in an asset file. The engine can spot a readiness call written directly into the template, but it cannot see inside a .js — and chart code almost always lives in one.


Several charts

Each chart registers itself as it starts. Order does not matter and no chart needs to know about the others, which is what makes this work across separate asset files.

// assets/js/report-charts.js

// Chart libraries are event-based, so turn "drawn" into a promise once and reuse it.
// Use whichever event yours raises when a series has finished rendering.
const drawn = (series) =>
    new Promise(resolve => series.events.once("rendered", resolve));

paragon.waitFor(drawn(revenueSeries));
paragon.waitFor(drawn(marginSeries));
paragon.waitFor(drawn(headcountSeries));

Data first, then a chart from it

Work started inside a tracked promise is registered before the earlier one is released, so the barrier stays closed across the whole chain.

paragon.waitFor(
    fetch("assets/data/regions.json")
        .then(response => response.json())
        .then(regions => {
            const chart = buildRegionChart(regions);
            return drawn(chart.series);
        })
);

An await chain reads the same way — the barrier holds until the whole function settles:

paragon.waitFor((async () => {
    const regions = await loadRegions();
    const chart = buildRegionChart(regions);
    await drawn(chart.series);
})());

Callback code with no promise

pending() hands back a release function. Call it when that piece is finished; calling it twice is harmless.

const release = paragon.pending();

map.on("tilesloaded", () => {
    labelTheMap();
    release();
});

Images the browser has not decoded yet

Loading is not decoding — a large image can still be blank when the page is otherwise idle.

document.querySelectorAll("img").forEach(image => {
    paragon.waitFor(image.decode().catch(() => {}));
});

Web fonts a script loads later

Fonts in the template's CSS are already waited for. One requested from script is not.

const face = new FontFace("Paragon Numerals", "url(assets/fonts/numerals.woff2)");

paragon.waitFor(
    face.load().then(loaded => document.fonts.add(loaded))
);

Failures

A rejected promise releases the barrier rather than holding it. The error is written to the console and the report is produced with that piece missing.

This is deliberate. A report with one empty chart shows you which chart broke; a render that hangs until RenderTimeout and throws ReportRenderTimeoutException tells you only that something, somewhere, never finished.

If a piece failing should fail the whole report, do that in your own code — let the promise reject after you have logged what you need, and check the output.


Traps

Register synchronously, or from inside tracked work. These two are safe:

paragon.waitFor(a);                       // registered now
paragon.waitFor(b.then(() => c));         // registered inside tracked work

This one is not:

setTimeout(() => paragon.waitFor(c), 50);  // may arrive after the gate has opened

The barrier allows one tick after the last piece finishes for follow-on work to appear. A timer longer than that can miss the window. Wrap the delay itself instead:

paragon.waitFor(new Promise(resolve => setTimeout(resolve, 50)).then(() => c));

Do not mix ready() into a barrier. ready() opens the gate immediately. A chart that calls it on completion, in a report where three charts are registered, captures the page when the fastest chart finishes.

A promise that never settles hangs the render. No resolve, no reject, no capture — you get ReportRenderTimeoutException after RenderTimeout. Give anything that might not answer its own deadline:

const withDeadline = (work, ms) => Promise.race([
    work,
    new Promise((_, reject) => setTimeout(() => reject(new Error("timed out")), ms))
]);

paragon.waitFor(withDeadline(slowThing(), 5000));

Animations are already off. The engine disables every CSS animation and transition, so nothing needs to be waited out. Turn your chart library's own animation off as well — most have a single setting for it — because an animating chart reports itself as drawn while it is still moving.


Checking it

Preview the report in the designer. If it comes out with a blank chart, the barrier is opening too early; if it times out, something registered never settled — the browser console names it.

See also Report template format — schema 2.0 for the manifest, and the Template draws asynchronously toggle on the report itself.