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.
// 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' }" />
<!-- 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:
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):
| Option | Type | What it does |
|---|---|---|
locale | string | Language of the prose and number format. Italian for it/it-IT, English otherwise. |
title | string | Replaces the generated headline. |
tone | 'storytelling' | 'professional' | 'executive' | Voice of the "In brief" section. Default 'storytelling'. |
theme | 'light' | 'dark' | 'auto' | Base look. Default dark. |
colors | StoryColors | Palette, 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. |
maxSections | number | Most 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). |
takeaways | boolean | Close on a "To sum up" section. Default true. |
maxEntities | number | Most rows in the barcode section. Default 40. |
columnLabels | Record<string, string> | Readable names per column, used in titles and prose. |
lockedMessage | string | Text 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.
| Format | How it reads |
|---|---|
scroll | A long article: pinned opening chart with changing text, numbered chapters, charts pinned while step cards scroll past. |
slides | A 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). |
chat | A 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. |
paper | A 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. |
timeline | Numbered 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. |
stories | Tap-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. |
quiz | Each 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. |
bento | Everything 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.
| Page | Address | What 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 | #/e | Every entity with its overall grade, filterable. |
| Method | #/method | Grade 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.
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
| Data | Opening chart |
|---|---|
| 40 years or more | Spiral, one turn every 25 years, peaks labeled, with a replay button |
| 10 to 39 years | Area line with anomalies marked |
| No time axis, 6+ groups of a summed metric | Treemap |
| No time axis, otherwise | Ranked 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
| Section | When | Chart |
|---|---|---|
In brief (brief) | Always, when there is something to say | Prose and bullets |
Turning points (turns) | 10+ years | Pinned line; cards highlight the biggest jumps, the worst fall and the records |
Decades (decades) | 60+ years | Radial bars, cards step through the top decades |
Race for first place (race) | 10+ years, 3–30 entities | Bump 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+ entities | Slope chart, first fifth of the period against the last, for the 4 biggest gains and losses |
Side by side (comparison) | 2–8 entities | Small multiples by decade |
Barcode (barcode) | Entities over time | One row per entity, one column per year, two sort orders |
Long quiet (quiet) | Many zero years | Range bars of the longest stretches at zero |
River (river) | Summed metric or on/off flags | Streamgraph of the top 6 |
Concentration (concentration) | Summed metric, 5+ entities | Treemap (donut when the opening is a treemap), "N of M make half the total" |
Relation (relation) | Two metrics | Scatter, with correlation strength in the prose |
Distribution (spread) | 30+ rows, varied values | Histogram, 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 swing | Radial bars by month |
Week (week) | Daily dates over 4+ weeks, with a real weekly swing | Bars Monday to Sunday, busiest day labeled |
To sum up (takeaways) | 2+ other sections | The opening sentence of the three strongest sections |
Map (map) | Countries or coordinates | Choropleth (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
- Lead-ins — every section after the first opens with one sentence handing over from the one before ("Behind the totals, a race was going on."), so the piece reads as an article, not a list of charts.
- Annotations — the points the prose names (peak, biggest jumps, last record, busiest weekday) are labeled on the chart, in the accent color.
- Section links — hovering a section shows a link icon next to its kicker. Each section has the id
ds-story-<section id>; opening the page with that hash scrolls to it once the story is mounted.
Choosing sections
Use the ids in brackets in the table above.
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
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.
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.