Data story

The same dataset as the auto-dashboard, told as a scrolling article: a pinned opening chart, sections chosen by the shape of the data, prose written from figures computed on your rows, and charts that animate as they come into view. No LLM, no server, deterministic. Licensed, like the auto-dashboard.

Quick start

Every framework component takes view="story". The rows go in data, as an array of records or a CSV/JSON string.

tsx
// React
<DatastripesDashboard data={rows} apiKey={key} view="story" storyOptions={{ locale: 'it-IT' }} />

// Vue
<DatastripesDashboard :data="rows" :api-key="key" view="story" :story-options="{ locale: 'it-IT' }" />

// Angular
<datastripes-dashboard [data]="rows" [apiKey]="key" view="story" [storyOptions]="{ locale: 'it-IT' }" />
html
<!-- Custom element: view and api-key as attributes, the rest as properties -->
<datastripes-dashboard id="story" view="story" api-key="ds_live_..."></datastripes-dashboard>
<script>
  const el = document.getElementById('story');
  el.data = rows;
  el.storyOptions = { locale: 'it-IT', theme: 'light' };
</script>

From a file, without a framework:

ts
import { mountAutoStory } from '@datastripes/dashboard';

const { story, generated, destroy } = await mountAutoStory(container, file, {
  apiKey: 'ds_live_...',
  locale: 'it-IT',
  theme: 'dark'
});
// generated === false when the license doesn't unlock it: nothing is mounted.

The story view never leaves the container empty: with no rows, without a license, or when rendering fails, it shows a short message instead (lockedMessage overrides the license one).

Options

storyOptions (or the options of mountAutoStory):

OptionTypeWhat it does
localestringLanguage of the prose and number format. Italian for it/it-IT, English otherwise.
titlestringReplaces the generated headline.
tone'storytelling' | 'professional' | 'executive'Voice of the "In brief" section. Default 'storytelling'.
theme'light' | 'dark' | 'auto'Base look. Default dark.
colorsStoryColorsPalette, accent, background, text and fonts. See Colors.
format'auto' | 'scroll' | 'slides' | 'chat' | 'paper' | 'timeline' | 'stories' | 'quiz' | 'bento' | 'site'Layout of the story. Default 'auto': picked from the dataset, so different data reads in different forms (never 'site', which is asked for by name). See Formats and Site.
routing'hash' | 'memory'Format 'site' only, a mountStoryInto option. 'hash' (default) puts the page in the URL so every page can be linked; 'memory' keeps it inside the component, for hosts that use the hash themselves.
maxSectionsnumberMost sections told besides the brief, summary and map; the weakest go. Default 7.
sections{ include?, exclude?, order? }Section ids to keep, drop, or put first (see Choosing sections).
takeawaysbooleanClose on a "To sum up" section. Default true.
maxEntitiesnumberMost rows in the barcode section. Default 40.
columnLabelsRecord<string, string>Readable names per column, used in titles and prose.
lockedMessagestringText shown when the license doesn't unlock the story.
narrate(section, story) => Promise<{ title?, body?, leadIn? }>Rewrite each section with your own LLM, in every wrapper and in mountAutoStory. Sections are sent in parallel; a failed call keeps the original text.

Formats

The same sections and figures, told three ways. Each format changes order, pacing and motion, not the findings.

FormatHow it reads
scrollA long article: pinned opening chart with changing text, numbered chapters, charts pinned while step cards scroll past.
slidesA deck, one finding per screen, in its own scroll-snapping area. The summary comes right after the opening (answer first). Arrow keys, Page Up/Down, Home/End and the buttons step through; steps play by themselves on the chart while a slide is on screen; layouts alternate (chart left, chart right, big figure, full-screen statement).
chatA conversation: each section opens on the reader's question (section.question), the answer types itself in bubble by bubble with the chart in a card; pointing at a step lights its points; a reply chip jumps to the next question.
paperA newspaper front page: dated masthead, the brief as lead story beside the hero chart, then articles in columns of falling width (two wide, three middling, then briefs) with the key figure as a pull quote.
timelineNumbered stops along a central line that fills as you scroll; each stop slides in from its side and draws its chart. Steps sit under the chart and light its points on hover.
storiesTap-through cards in a phone-sized frame: segmented progress bars, tap right for next and left for back, hold or press Pause to stop, arrow keys too. Each card advances after 7 seconds (not with reduced motion). Answer first, like the deck.
quizEach finding as a question with its answer blurred: the reader guesses, presses Reveal, and the answer sharpens, the figures count up and the chart lights the points the prose names. A counter tracks what's revealed; "Reveal all" opens everything.
bentoEverything at once on a grid of tiles: cover and hero large, the strongest findings big, key figures as accent-colored tiles. Clicking a tile (or Enter) opens it full width with the whole text; one at a time.

Pin one with format: 'scroll'; mountStoryInto(container, story, { format }) also renders an existing story in another format. The slides format takes the full viewport height.

Site

format: 'site' turns a dataset of entities (countries, regions, schools…) and measures into a small website, in the spirit of an indicator tracker: the story as home page, then pages addressed by the URL hash.

PageAddressWhat it shows
Home#/The story, then a card per measure (with its leader) and the best-graded entities.
Topic#/t/{measure}Leading and trailing ten with grades, the median and year read, then the measure told as a story of its own, each in a different format.
Profile#/e/{entity}Overall grade, a computed summary (above the median in k of n, strongest and weakest, how many improved), and a card per measure: grade, value, rank, median and trend line. Previous and next entity.
Index#/eEvery entity with its overall grade, filterable.
Method#/methodGrade legend, how grades are computed, which measures rank lower-is-better, and the notes about the data.

Grades split each ranking into fifths: A is the best fifth, E the worst; the overall grade averages them. Rankings read the latest year at least half the entities report. Measures that name a harm (emissions, deaths, poverty, military spending…) rank lowest first, and the method page says so. A header search jumps to any profile. The whole site shares one mood, so its pages belong together. It is a paid format, like every format beyond scroll and slides.

Colors and fonts

Anything you leave out keeps the default for the chosen theme.

ts
storyOptions: {
  theme: 'light',
  colors: {
    palette: ['#0f766e', '#f59e0b', '#6366f1'], // series: charts, legend, small multiples
    accent: '#0f766e',                          // kickers, progress bar, active step, big figures
    background: '#fffdf7',
    text: '#1c1917',                            // headings and figures
    muted: '#57534e',                           // body prose
    fontFamily: 'Inter, sans-serif',
    displayFont: 'Georgia, serif'               // large titles and numbers
  }
}

In React the dashboard theme prop colors the story too: palette, primaryColor (as accent) and fontFamily. storyOptions.colors wins where both are set.

For finer control, override the CSS variables on .ds-story: --ds-story-bg, --ds-story-text, --ds-story-strong, --ds-story-muted, --ds-story-faint, --ds-story-line, --ds-story-control, --ds-story-accent, --ds-story-font, --ds-story-display.

How the story is built

Columns are profiled as in the auto-dashboard: the main metric, the main grouping (an "entity": countries, shops, regions…), the time axis. Every section below is written only when the data supports it; a section with nothing to say is left out.

Opening

DataOpening chart
40 years or moreSpiral, one turn every 25 years, peaks labeled, with a replay button
10 to 39 yearsArea line with anomalies marked
No time axis, 6+ groups of a summed metricTreemap
No time axis, otherwiseRanked horizontal bars

The headline comes from the strongest finding: growth ("×3.2 since 2000"), decline ("down 40% since 1990"), the peak year, or the dominant entity. The text beside the opening chart changes as you scroll.

Sections

SectionWhenChart
In brief (brief)Always, when there is something to sayProse and bullets
Turning points (turns)10+ yearsPinned line; cards highlight the biggest jumps, the worst fall and the records
Decades (decades)60+ yearsRadial bars, cards step through the top decades
Race for first place (race)10+ years, 3–30 entitiesBump chart of the top 5's yearly rank (1 on top, names at the line ends), with the number of changes at the top set large
Then and now (movers)10+ years, 3+ entitiesSlope chart, first fifth of the period against the last, for the 4 biggest gains and losses
Side by side (comparison)2–8 entitiesSmall multiples by decade
Barcode (barcode)Entities over timeOne row per entity, one column per year, two sort orders
Long quiet (quiet)Many zero yearsRange bars of the longest stretches at zero
River (river)Summed metric or on/off flagsStreamgraph of the top 6
Concentration (concentration)Summed metric, 5+ entitiesTreemap (donut when the opening is a treemap), "N of M make half the total"
Relation (relation)Two metricsScatter, with correlation strength in the prose
Distribution (spread)30+ rows, varied valuesHistogram, typical value and 10–90% range
Durations / Links (durations, links)A sparse event column (e.g. names of wars)Beeswarm of how long each lasted; chord of entities sharing events
Clock (clock)A month column or daily/monthly dates, with a real seasonal swingRadial bars by month
Week (week)Daily dates over 4+ weeks, with a real weekly swingBars Monday to Sunday, busiest day labeled
To sum up (takeaways)2+ other sectionsThe opening sentence of the three strongest sections
Map (map)Countries or coordinatesChoropleth (with time play) or points; always last

Sections are ordered by how much they have to say (how sharp the jump, how concentrated the total, how strong the correlation), so two datasets don't produce the same sequence. Titles have several phrasings; the dataset picks one, so the same data always reads the same and different data reads differently. Caveats (entities at zero over the whole span, coverage notes) go in the "About the data" footer.

Reading aids

Choosing sections

Use the ids in brackets in the table above.

ts
storyOptions: {
  sections: {
    exclude: ['barcode', 'spread'],  // never these
    order: ['race', 'turns']         // these first; the rest follow by strength
  },
  takeaways: false
}

// Or a fixed short story:
storyOptions: { sections: { include: ['brief', 'turns', 'map'] } }

A section listed in include or order still appears only when the data supports it.

Rewriting the prose with an LLM

ts
storyOptions: {
  narrate: async (section, story) => {
    const res = await fetch('/api/rewrite', { method: 'POST', body: JSON.stringify({ section, title: story.title }) });
    return res.json(); // { title?, body?, leadIn? }: keep **bold** around figures
  }
}

Each section carries its computed figures in body, so the model rewrites rather than invents. In React the story is remounted when storyOptions changes by value; a new narrate function alone doesn't trigger it.

Generating without rendering

A story is plain JSON holding its own aggregated rows: store it, edit its prose, or render it later.

ts
import { generateStory, mountStoryInto } from '@datastripes/dashboard';

const story = generateStory(rows, license, { locale: 'it-IT' }); // throws AutoDashboardLicenseError on trial
story.sections[1].body = 'My own text, **bold** for figures.';
const dispose = mountStoryInto(container, story, {
  chart: { mode: 'light' },
  colors: { accent: '#0f766e' },
  locale: 'it-IT'
});

Since a story is a finished piece, not a live view, dashboard filters don't apply to it. Charts start as they scroll into view, and motion is skipped when the reader asks for reduced motion.