Enarratio gallery

Theme default. Every chart below was drawn on the server and reads without scripts; the enhancement layer adds hover details, keyboard navigation, filters and brushing.

Theme check: passes with 1 finding

Entries per year by type, counted from one row per entry: the CV's overview chart.

Entries per year

  • Publications
  • Grants
  • Talks
Year 2019, Publications: 3Year 2019, Talks: 1Year 2020, Publications: 4Year 2020, Grants: 1Year 2021, Publications: 5Year 2021, Talks: 2Year 2022, Publications: 6Year 2022, Grants: 2Year 2022, Talks: 3Year 2023, Publications: 4Year 2023, Talks: 4Year 2024, Publications: 7Year 2024, Grants: 1Year 2024, Talks: 2
Illustrative data.
Data table
Data for: Entries per year
YearPublicationsGrantsTalks
2019301
2020410
2021502
2022623
2023404
2024712

Source: examples/stacked-bar.ts
// Entries per year by type, counted from one row per entry: the CV's overview chart.
import { barChart } from "enarratio";

const entries = [
  ...Array.from({ length: 3 }, () => ({ year: 2019, type: "Publications" })),
  { year: 2019, type: "Talks" },
  ...Array.from({ length: 4 }, () => ({ year: 2020, type: "Publications" })),
  { year: 2020, type: "Grants" },
  ...Array.from({ length: 2 }, () => ({ year: 2021, type: "Talks" })),
  ...Array.from({ length: 5 }, () => ({ year: 2021, type: "Publications" })),
  ...Array.from({ length: 6 }, () => ({ year: 2022, type: "Publications" })),
  ...Array.from({ length: 2 }, () => ({ year: 2022, type: "Grants" })),
  ...Array.from({ length: 3 }, () => ({ year: 2022, type: "Talks" })),
  ...Array.from({ length: 4 }, () => ({ year: 2023, type: "Publications" })),
  ...Array.from({ length: 4 }, () => ({ year: 2023, type: "Talks" })),
  ...Array.from({ length: 7 }, () => ({ year: 2024, type: "Publications" })),
  { year: 2024, type: "Grants" },
  ...Array.from({ length: 2 }, () => ({ year: 2024, type: "Talks" })),
];

export default barChart({
  id: "entries-by-year",
  data: entries,
  x: "year",
  series: "type",
  seriesDomain: ["Publications", "Grants", "Talks"],
  directLabels: true,
  xLabel: "Year",
  yLabel: "Entries",
  title: "Entries per year",
  alt: "Stacked bars of CV entries per year from 2019 to 2024. Publications lead every year except 2023, when talks matched them; 2024 is the busiest year with 10 entries.",
  caption: "Illustrative data.",
});

Selecting a year: bars filter by category, link to a filtered page without script, and thin their labels.

Entries per year, 2005 to 2026

  • Publications
  • Grants
  • Talks
Illustrative data. Clicking a bar selects its year; with scripts off, each bar links to that year.
Data table
Data for: Entries per year, 2005 to 2026
YearPublicationsGrantsTalks
2005031
2006210
2007444
2008123
2009302
2010031
2011210
2012444
2013123
2014302
2015031
2016210
2017444
2018123
2019302
2020031
2021210
2022444
2023123
2024302
2025031
2026210

Source: examples/years.ts
// Selecting a year: bars filter by category, link to a filtered page without script, and thin their labels.
import { barChart } from "enarratio";

const types = ["Publications", "Grants", "Talks"] as const;
const entries = Array.from({ length: 22 }, (_, i) => 2005 + i).flatMap((year, i) =>
  types.flatMap((type, t) =>
    Array.from({ length: (i * (t + 2) + t * 3) % 5 }, () => ({ year, type })),
  ),
);

export default barChart({
  id: "entries-by-year-select",
  data: entries,
  x: "year",
  series: "type",
  seriesDomain: [...types],
  xDomain: Array.from({ length: 22 }, (_, i) => 2005 + i),
  filterBy: "x",
  href: (year) => `?year=${year}`,
  maxXTicks: 8,
  xLabel: "Year",
  yLabel: "Entries",
  title: "Entries per year, 2005 to 2026",
  alt: "Stacked bars of CV entries per year from 2005 to 2026, by type. Every year has between 3 and 12 entries, peaking at 12 every fifth year: 2007, 2012, 2017 and 2022.",
  caption:
    "Illustrative data. Clicking a bar selects its year; with scripts off, each bar links to that year.",
});

Two measurements per category, side by side.

Time to result by assay

  • Manual
  • Automated
Assay Plaque, Manual: 72Assay Plaque, Automated: 48Assay TCID50, Manual: 120Assay TCID50, Automated: 96Assay qPCR, Manual: 6Assay qPCR, Automated: 4
Illustrative data.
Data table
Data for: Time to result by assay
AssayManualAutomated
Plaque7248
TCID5012096
qPCR64

Source: examples/grouped-bar.ts
// Two measurements per category, side by side.
import { barChart } from "enarratio";

const results = [
  { assay: "Plaque", method: "Manual", hours: 72 },
  { assay: "Plaque", method: "Automated", hours: 48 },
  { assay: "TCID50", method: "Manual", hours: 120 },
  { assay: "TCID50", method: "Automated", hours: 96 },
  { assay: "qPCR", method: "Manual", hours: 6 },
  { assay: "qPCR", method: "Automated", hours: 4 },
];

export default barChart({
  data: results,
  x: "assay",
  y: "hours",
  series: "method",
  layout: "grouped",
  xLabel: "Assay",
  yLabel: "Hours to result",
  title: "Time to result by assay",
  alt: "Grouped bars of hours to result for three assays. Automation saves a day on plaque assays (72 to 48 hours) and TCID50 (120 to 96), and two hours on qPCR (6 to 4).",
  caption: "Illustrative data.",
});

A ranking with long category names reads best as horizontal bars.

Most viewed pages

page Teaching and mentoring: 1,840page Publications: 1,320page Influenza surveillance notes: 960page Curriculum vitae: 720page Contact: 310
Data table
Data for: Most viewed pages
pageViews
Teaching and mentoring1,840
Publications1,320
Influenza surveillance notes960
Curriculum vitae720
Contact310

Source: examples/horizontal-bar.ts
// A ranking with long category names reads best as horizontal bars.
import { barChart } from "enarratio";

const pages = [
  { page: "Teaching and mentoring", views: 1840 },
  { page: "Publications", views: 1320 },
  { page: "Influenza surveillance notes", views: 960 },
  { page: "Curriculum vitae", views: 720 },
  { page: "Contact", views: 310 },
];

export default barChart({
  data: pages,
  x: "page",
  y: "views",
  orientation: "horizontal",
  xLabel: null,
  yLabel: "Views",
  title: "Most viewed pages",
  alt: "Horizontal bars ranking five pages by views: Teaching and mentoring leads with 1,840, then Publications 1,320, surveillance notes 960, the CV 720 and Contact 310.",
});

Weekly counts over time, with a gap, a threshold and an event marker.

Weekly positive tests by county

  • Travis
  • Hays
  • Williamson
Travis, Week starting 2025-10-06: 42Hays, Week starting 2025-10-06: 18Williamson, Week starting 2025-10-06: 27Travis, Week starting 2025-10-13: 42Hays, Week starting 2025-10-13: 18Williamson, Week starting 2025-10-13: 27Travis, Week starting 2025-10-20: 42Hays, Week starting 2025-10-20: 18Williamson, Week starting 2025-10-20: 27Travis, Week starting 2025-10-27: 43Hays, Week starting 2025-10-27: 18Williamson, Week starting 2025-10-27: 27Travis, Week starting 2025-11-03: 44Hays, Week starting 2025-11-03: 19Williamson, Week starting 2025-11-03: 29Travis, Week starting 2025-11-10: 48Hays, Week starting 2025-11-10: 21Williamson, Week starting 2025-11-10: 31Travis, Week starting 2025-11-17: 56Williamson, Week starting 2025-11-17: 36Travis, Week starting 2025-11-24: 66Hays, Week starting 2025-11-24: 28Williamson, Week starting 2025-11-24: 43Travis, Week starting 2025-12-01: 76Hays, Week starting 2025-12-01: 32Williamson, Week starting 2025-12-01: 49Travis, Week starting 2025-12-08: 80Hays, Week starting 2025-12-08: 34Williamson, Week starting 2025-12-08: 51Travis, Week starting 2025-12-15: 76Hays, Week starting 2025-12-15: 32Williamson, Week starting 2025-12-15: 49Travis, Week starting 2025-12-22: 66Hays, Week starting 2025-12-22: 28Williamson, Week starting 2025-12-22: 43Travis, Week starting 2025-12-29: 56Hays, Week starting 2025-12-29: 24Williamson, Week starting 2025-12-29: 36Travis, Week starting 2026-01-05: 48Hays, Week starting 2026-01-05: 21Williamson, Week starting 2026-01-05: 31Travis, Week starting 2026-01-12: 44Hays, Week starting 2026-01-12: 19Williamson, Week starting 2026-01-12: 29Travis, Week starting 2026-01-19: 43Hays, Week starting 2026-01-19: 18Williamson, Week starting 2026-01-19: 27Alert thresholdHolidayTravisHaysWilliamson
Illustrative data. Drag across the chart, or hold Shift with the arrow keys, to pick a range.
Data table
Data for: Weekly positive tests by county
Week startingTravisHaysWilliamson
2025-10-06421827
2025-10-13421827
2025-10-20421827
2025-10-27431827
2025-11-03441929
2025-11-10482131
2025-11-175636
2025-11-24662843
2025-12-01763249
2025-12-08803451
2025-12-15763249
2025-12-22662843
2025-12-29562436
2026-01-05482131
2026-01-12441929
2026-01-19431827

Source: examples/line.ts
// Weekly counts over time, with a gap, a threshold and an event marker.
import { lineChart } from "enarratio";

const counties = ["Travis", "Hays", "Williamson"] as const;
const base = { Travis: 42, Hays: 18, Williamson: 27 };
const weekly = counties.flatMap((county) =>
  Array.from({ length: 16 }, (_, week) => {
    const wave = Math.round(base[county] * (1 + 0.9 * Math.exp(-(((week - 9) / 3) ** 2))));
    // Hays did not report in week 6: a gap, not a zero.
    const cases = county === "Hays" && week === 6 ? null : wave;
    return { week: new Date(Date.UTC(2025, 9, 6 + week * 7)), county, cases };
  }),
);

export default lineChart({
  id: "weekly-cases",
  data: weekly,
  x: "week",
  y: "cases",
  series: "county",
  xLabel: "Week starting",
  yLabel: "Positive tests",
  references: [{ y: 60, label: "Alert threshold" }],
  markers: [{ x: "2025-11-24", label: "Holiday" }],
  directLabels: true,
  title: "Weekly positive tests by county",
  alt: "Lines of weekly positive tests in three counties from October 2025 to January 2026. All three peak in the week of 8 December; Travis peaks near 80, above the alert threshold of 60, while Hays and Williamson stay below it. Hays did not report in the week of 17 November.",
  caption:
    "Illustrative data. Drag across the chart, or hold Shift with the arrow keys, to pick a range.",
});

Stacked areas: parts of a whole over time.

Where the week goes

  • Research
  • Teaching
  • Service
Research, Month 2025-09-01: 22Teaching, Month 2025-09-01: 12Service, Month 2025-09-01: 6Research, Month 2025-10-01: 20Teaching, Month 2025-10-01: 14Service, Month 2025-10-01: 6Research, Month 2025-11-01: 18Teaching, Month 2025-11-01: 16Service, Month 2025-11-01: 7Research, Month 2025-12-01: 24Teaching, Month 2025-12-01: 10Service, Month 2025-12-01: 6Research, Month 2026-01-01: 26Teaching, Month 2026-01-01: 9Service, Month 2026-01-01: 5Research, Month 2026-02-01: 25Teaching, Month 2026-02-01: 12Service, Month 2026-02-01: 6
Illustrative data.
Data table
Data for: Where the week goes
MonthResearchTeachingService
2025-09-0122126
2025-10-0120146
2025-11-0118167
2025-12-0124106
2026-01-012695
2026-02-0125126

Source: examples/area.ts
// Stacked areas: parts of a whole over time.
import { areaChart } from "enarratio";

const activities = ["Research", "Teaching", "Service"] as const;
const hours = {
  Research: [22, 20, 18, 24, 26, 25],
  Teaching: [12, 14, 16, 10, 9, 12],
  Service: [6, 6, 7, 6, 5, 6],
};
const monthly = activities.flatMap((activity) =>
  hours[activity].map((h, i) => ({
    month: new Date(Date.UTC(2025, 8 + i, 1)),
    activity,
    hours: h,
  })),
);

export default areaChart({
  data: monthly,
  x: "month",
  y: "hours",
  series: "activity",
  xLabel: "Month",
  yLabel: "Hours per week",
  title: "Where the week goes",
  alt: "Stacked areas of weekly hours by activity from September 2025 to February 2026. Research is the largest share throughout, rising to 26 hours in January; teaching peaks at 16 hours in November; service stays near 6.",
  caption: "Illustrative data.",
});

Two measurements per sample, grouped, with a regression line on a log axis.

Ct value against infectious titer

  • 2.3.4.4b
  • 2.3.2.1c
A1: 2.3.4.4b, Cycle threshold (Ct) 18.2, Infectious titer (PFU/mL) 5,200,000A2: 2.3.4.4b, Cycle threshold (Ct) 21.5, Infectious titer (PFU/mL) 610,000A3: 2.3.4.4b, Cycle threshold (Ct) 24.9, Infectious titer (PFU/mL) 48,000A4: 2.3.4.4b, Cycle threshold (Ct) 27.3, Infectious titer (PFU/mL) 9,500A5: 2.3.4.4b, Cycle threshold (Ct) 30.8, Infectious titer (PFU/mL) 720B1: 2.3.2.1c, Cycle threshold (Ct) 19.4, Infectious titer (PFU/mL) 2,100,000B2: 2.3.2.1c, Cycle threshold (Ct) 22.8, Infectious titer (PFU/mL) 190,000B3: 2.3.2.1c, Cycle threshold (Ct) 26.1, Infectious titer (PFU/mL) 24,000B4: 2.3.2.1c, Cycle threshold (Ct) 29.6, Infectious titer (PFU/mL) 1,600
Illustrative data. Each series has its own symbol as well as its own color.
Data table
Data for: Ct value against infectious titer
PointSeriesCycle threshold (Ct)Infectious titer (PFU/mL)
A12.3.4.4b18.25,200,000
A22.3.4.4b21.5610,000
A32.3.4.4b24.948,000
A42.3.4.4b27.39,500
A52.3.4.4b30.8720
B12.3.2.1c19.42,100,000
B22.3.2.1c22.8190,000
B32.3.2.1c26.124,000
B42.3.2.1c29.61,600

Source: examples/scatter.ts
// Two measurements per sample, grouped, with a regression line on a log axis.
import { scatterPlot } from "enarratio";

const isolates = [
  { id: "A1", clade: "2.3.4.4b", ct: 18.2, titer: 5.2e6 },
  { id: "A2", clade: "2.3.4.4b", ct: 21.5, titer: 6.1e5 },
  { id: "A3", clade: "2.3.4.4b", ct: 24.9, titer: 4.8e4 },
  { id: "A4", clade: "2.3.4.4b", ct: 27.3, titer: 9.5e3 },
  { id: "A5", clade: "2.3.4.4b", ct: 30.8, titer: 7.2e2 },
  { id: "B1", clade: "2.3.2.1c", ct: 19.4, titer: 2.1e6 },
  { id: "B2", clade: "2.3.2.1c", ct: 22.8, titer: 1.9e5 },
  { id: "B3", clade: "2.3.2.1c", ct: 26.1, titer: 2.4e4 },
  { id: "B4", clade: "2.3.2.1c", ct: 29.6, titer: 1.6e3 },
];

export default scatterPlot({
  data: isolates,
  x: "ct",
  y: "titer",
  series: "clade",
  label: "id",
  yType: "log",
  regression: true,
  xLabel: "Cycle threshold (Ct)",
  yLabel: "Infectious titer (PFU/mL)",
  title: "Ct value against infectious titer",
  alt: "Scatter plot of nine isolates from two clades: infectious titer falls about tenfold for every 3.3 Ct cycles in both clades, from about 5 million PFU/mL at Ct 18 to about 700 at Ct 31.",
  caption: "Illustrative data. Each series has its own symbol as well as its own color.",
});

A grid of values on the theme's sequential ramp.

Retention by cohort

  • below 50
  • 50 to 60
  • 60 to 70
  • 70 to 85
  • 85 or more
Cohort Jan, Months since joining M0: 100 % retainedCohort Jan, Months since joining M1: 82 % retainedCohort Jan, Months since joining M2: 67 % retainedCohort Jan, Months since joining M3: 55 % retainedCohort Jan, Months since joining M4: 45 % retainedCohort Feb, Months since joining M0: 100 % retainedCohort Feb, Months since joining M1: 80 % retainedCohort Feb, Months since joining M2: 65 % retainedCohort Feb, Months since joining M3: 53 % retainedCohort Mar, Months since joining M0: 100 % retainedCohort Mar, Months since joining M1: 78 % retainedCohort Mar, Months since joining M2: 63 % retainedCohort Apr, Months since joining M0: 100 % retainedCohort Apr, Months since joining M1: 76 % retainedCohort May, Months since joining M0: 100 % retained10082675545100806553100786310076100
Illustrative data.
Data table
Data for: Retention by cohort
Cohort \ Months since joiningM0M1M2M3M4
Jan10082675545
Feb100806553
Mar1007863
Apr10076
May100

Source: examples/heatmap.ts
// A grid of values on the theme's sequential ramp.
import { heatmap } from "enarratio";

const cohorts = ["Jan", "Feb", "Mar", "Apr", "May"];
const retention = cohorts.flatMap((cohort, c) =>
  Array.from({ length: 5 - c }, (_, m) => ({
    cohort,
    month: `M${m}`,
    retained: m === 0 ? 100 : Math.round(100 * 0.82 ** m - c * 2),
  })),
);

export default heatmap({
  data: retention,
  x: "month",
  y: "cohort",
  value: "retained",
  valueLabel: "% retained",
  xDomain: ["M0", "M1", "M2", "M3", "M4"],
  thresholds: [50, 60, 70, 85],
  xLabel: "Months since joining",
  yLabel: "Cohort",
  title: "Retention by cohort",
  alt: "Retention heatmap for five monthly cohorts: every cohort starts at 100% and falls about 18 points a month, reaching 45% at month four for the January cohort.",
  caption: "Illustrative data.",
});

A force-directed network, laid out on the server.

Co-authorship network

  • Virology
  • Epidemiology
  • Statistics
  • Public health
Edwards (Virology): 4 connectionsOkafor (Virology): 2 connectionsLindqvist (Virology): 3 connectionsRamos (Epidemiology): 4 connectionsChen (Epidemiology): 3 connectionsHaddad (Epidemiology): 3 connectionsNovak (Statistics): 3 connectionsIbarra (Statistics): 2 connectionsMbeki (Public health): 2 connectionsSato (Public health): 2 connectionsEdwardsOkaforLindqvistRamosChenHaddadNovakIbarraMbekiSato
Illustrative data. Links are co-authored papers.
Data table
Data for: Co-authorship network
NodeGroupConnected to
EdwardsVirologyOkafor, Lindqvist, Ramos, Novak
OkaforVirologyEdwards, Lindqvist
LindqvistVirologyEdwards, Okafor, Ibarra
RamosEpidemiologyEdwards, Chen, Haddad, Sato
ChenEpidemiologyRamos, Haddad, Novak
HaddadEpidemiologyChen, Ramos, Mbeki
NovakStatisticsChen, Ibarra, Edwards
IbarraStatisticsNovak, Lindqvist
MbekiPublic healthHaddad, Sato
SatoPublic healthMbeki, Ramos

Source: examples/network.ts
// A force-directed network, laid out on the server.
import { networkChart } from "enarratio";

const people = [
  ["Edwards", "Virology"],
  ["Okafor", "Virology"],
  ["Lindqvist", "Virology"],
  ["Ramos", "Epidemiology"],
  ["Chen", "Epidemiology"],
  ["Haddad", "Epidemiology"],
  ["Novak", "Statistics"],
  ["Ibarra", "Statistics"],
  ["Mbeki", "Public health"],
  ["Sato", "Public health"],
] as const;
const pairs = [
  ["Edwards", "Okafor"],
  ["Edwards", "Lindqvist"],
  ["Okafor", "Lindqvist"],
  ["Edwards", "Ramos"],
  ["Ramos", "Chen"],
  ["Chen", "Haddad"],
  ["Ramos", "Haddad"],
  ["Chen", "Novak"],
  ["Novak", "Ibarra"],
  ["Edwards", "Novak"],
  ["Haddad", "Mbeki"],
  ["Mbeki", "Sato"],
  ["Ramos", "Sato"],
  ["Lindqvist", "Ibarra"],
] as const;

export default networkChart({
  nodes: people.map(([id, group]) => ({ id, group })),
  links: pairs.map(([source, target]) => ({ source, target })),
  title: "Co-authorship network",
  alt: "Network of ten co-authors in four fields. Edwards and Ramos are the hubs, each with four collaborators; Edwards links the virologists to statistics and epidemiology, and Ramos links epidemiology to public health.",
  caption: "Illustrative data. Links are co-authored papers.",
});

Hemagglutination inhibition titers by timepoint on a two-fold dilution axis.

HAI titers after vaccination

LODTimepoint Day 0: below the limit of detection (<1:10)Timepoint Day 0: below the limit of detection (<1:10)Timepoint Day 0: 1:10Timepoint Day 0: 1:10Timepoint Day 0: 1:10Timepoint Day 0: 1:20Timepoint Day 0: 1:20Timepoint Day 0: 1:40Timepoint Day 0: 1:10Timepoint Day 0: 1:10Timepoint Day 0: 1:20Timepoint Day 0: 1:10Timepoint Day 14: 1:40Timepoint Day 14: 1:80Timepoint Day 14: 1:80Timepoint Day 14: 1:160Timepoint Day 14: 1:160Timepoint Day 14: 1:160Timepoint Day 14: 1:320Timepoint Day 14: 1:80Timepoint Day 14: 1:40Timepoint Day 14: 1:160Timepoint Day 14: 1:320Timepoint Day 14: 1:640Timepoint Day 28: 1:80Timepoint Day 28: 1:160Timepoint Day 28: 1:160Timepoint Day 28: 1:320Timepoint Day 28: 1:320Timepoint Day 28: 1:320Timepoint Day 28: 1:640Timepoint Day 28: 1:160Timepoint Day 28: 1:80Timepoint Day 28: 1:320Timepoint Day 28: 1:640Timepoint Day 28: 1:1280Timepoint Day 180: 1:20Timepoint Day 180: 1:40Timepoint Day 180: 1:40Timepoint Day 180: 1:80Timepoint Day 180: 1:80Timepoint Day 180: 1:80Timepoint Day 180: 1:160Timepoint Day 180: 1:40Timepoint Day 180: 1:20Timepoint Day 180: 1:80Timepoint Day 180: 1:160Timepoint Day 180: 1:320
Illustrative data. Bars are geometric mean titers with 95% confidence intervals; hollow points are below the limit of detection, plotted at half the limit.
Data table
Data for: HAI titers after vaccination
TimepointnGMT95% CIBelow LODTiters
Day 01211.98.1 to 17.42<10, <10, 10, 10, 10, 20, 20, 40, 10, 10, 20, 10
Day 141213578.8 to 230040, 80, 80, 160, 160, 160, 320, 80, 40, 160, 320, 640
Day 2812269158 to 460080, 160, 160, 320, 320, 320, 640, 160, 80, 320, 640, 1280
Day 1801267.339.4 to 115020, 40, 40, 80, 80, 80, 160, 40, 20, 80, 160, 320

Source: examples/titer.ts
// Hemagglutination inhibition titers by timepoint on a two-fold dilution axis.
import { titerPlot } from "enarratio";

const titers: Record<string, number[]> = {
  "Day 0": [5, 5, 10, 10, 10, 20, 20, 40, 10, 10, 20, 10],
  "Day 14": [40, 80, 80, 160, 160, 160, 320, 80, 40, 160, 320, 640],
  "Day 28": [80, 160, 160, 320, 320, 320, 640, 160, 80, 320, 640, 1280],
  "Day 180": [20, 40, 40, 80, 80, 80, 160, 40, 20, 80, 160, 320],
};
const sera = Object.entries(titers).flatMap(([timepoint, values]) =>
  values.map((hai) => ({ timepoint, hai })),
);

export default titerPlot({
  data: sera,
  group: "timepoint",
  titer: "hai",
  dilution: { start: 10, factor: 2 },
  groupLabel: "Timepoint",
  titerLabel: "HAI titer",
  title: "HAI titers after vaccination",
  alt: "HAI titers for 12 participants at four timepoints. The geometric mean titer rises from about 12 at day 0 to about 270 at day 28, then wanes to about 67 by day 180. Two day 0 samples were below the 1:10 limit of detection.",
  caption:
    "Illustrative data. Bars are geometric mean titers with 95% confidence intervals; hollow points are below the limit of detection, plotted at half the limit.",
});

A viral genome map: features to scale, arrows by strand, overlaps stacked into lanes.

HIV-1 HXB2 genome

  • Gene
  • Accessory
  • Antisense
  • LTR
5' LTR (LTR): 1 to 634, 634 nt, no strandgag (Gene): 790 to 2,292, 1,503 nt, forward strandpol (Gene): 2,085 to 5,096, 3,012 nt, forward strandvif (Accessory): 5,041 to 5,619, 579 nt, forward strandvpr (Accessory): 5,559 to 5,850, 292 nt, forward strandtat (Accessory): 5,831 to 6,045, 215 nt, forward strandrev (Accessory): 5,970 to 6,045, 76 nt, forward strandvpu (Accessory): 6,062 to 6,310, 249 nt, forward strandenv (Gene): 6,225 to 8,795, 2,571 nt, forward strandnef (Accessory): 8,797 to 9,417, 621 nt, forward strand3' LTR (LTR): 9,086 to 9,719, 634 nt, no strandasp (Antisense): 7,270 to 8,260, 991 nt, reverse strandgagpolvifenvnefasp
Coordinates approximate the HXB2 reference (GenBank K03455) and are rounded for display.
Data table
Data for: HIV-1 HXB2 genome
FeatureTypeStartEndLength (nt)Strand
5' LTRLTR1634634
gagGene79022921503+
polGene208550963012+
vifAccessory50415619579+
vprAccessory55595850292+
tatAccessory58316045215+
revAccessory5970604576+
vpuAccessory60626310249+
envGene622587952571+
aspAntisense72708260991-
nefAccessory87979417621+
3' LTRLTR90869719634

Source: examples/genome.ts
// A viral genome map: features to scale, arrows by strand, overlaps stacked into lanes.
import { genomeTrack } from "enarratio";

export default genomeTrack({
  length: 9719,
  features: [
    { name: "5' LTR", start: 1, end: 634, type: "LTR" },
    { name: "gag", start: 790, end: 2292, strand: 1, type: "Gene" },
    { name: "pol", start: 2085, end: 5096, strand: 1, type: "Gene" },
    { name: "vif", start: 5041, end: 5619, strand: 1, type: "Accessory" },
    { name: "vpr", start: 5559, end: 5850, strand: 1, type: "Accessory" },
    { name: "tat", start: 5831, end: 6045, strand: 1, type: "Accessory" },
    { name: "rev", start: 5970, end: 6045, strand: 1, type: "Accessory" },
    { name: "vpu", start: 6062, end: 6310, strand: 1, type: "Accessory" },
    { name: "env", start: 6225, end: 8795, strand: 1, type: "Gene" },
    { name: "nef", start: 8797, end: 9417, strand: 1, type: "Accessory" },
    { name: "3' LTR", start: 9086, end: 9719, type: "LTR" },
    { name: "asp", start: 7270, end: 8260, strand: -1, type: "Antisense" },
  ],
  typeDomain: ["Gene", "Accessory", "Antisense", "LTR"],
  title: "HIV-1 HXB2 genome",
  alt: "Map of the 9,719-nucleotide HIV-1 HXB2 genome: long terminal repeats at both ends; the gag, pol and env genes on the forward strand, overlapping at their junctions; six accessory genes between pol and env and after env; and the antisense asp gene on the reverse strand inside env.",
  caption:
    "Coordinates approximate the HXB2 reference (GenBank K03455) and are rounded for display.",
});

A trend beside a number, with a text alternative generated from the data.

Entries per year, 2019 to 2024: 6 values, from 4 to 10; lowest 4, highest 11.

Source: examples/sparkline.ts
// A trend beside a number, with a text alternative generated from the data.
import { sparkline } from "enarratio";

export default sparkline({
  values: [4, 5, 7, 11, 8, 10],
  label: "Entries per year, 2019 to 2024",
  area: true,
});

Small primitives with the same styling: progress rings and an uptime strip.

Chapters drafted: 7 of 12 (58%)58% Tests passing: 92%92% Period 1: no dataPeriod 2: no dataPeriod 3: no dataPeriod 4: no dataPeriod 5: upPeriod 6: upPeriod 7: upPeriod 8: upPeriod 9: upPeriod 10: upPeriod 11: upPeriod 12: upPeriod 13: upPeriod 14: upPeriod 15: upPeriod 16: upPeriod 17: upPeriod 18: upPeriod 19: upPeriod 20: upPeriod 21: upPeriod 22: upPeriod 23: upPeriod 24: upPeriod 25: upPeriod 26: upPeriod 27: upPeriod 28: upPeriod 29: upPeriod 30: degradedPeriod 31: downPeriod 32: degradedPeriod 33: upPeriod 34: upPeriod 35: upPeriod 36: upPeriod 37: upPeriod 38: upPeriod 39: upPeriod 40: upPeriod 41: upPeriod 42: upPeriod 43: upPeriod 44: upPeriod 45: upPeriod 46: upPeriod 47: upPeriod 48: up

Source: examples/primitives.ts
// Small primitives with the same styling: progress rings and an uptime strip.
import { progressRing, type UptimeStatus, uptimeStrip } from "enarratio";

const hours = Array.from({ length: 48 }, (_, i): UptimeStatus => {
  if (i < 4) return "unknown";
  if (i === 30) return "down";
  return i === 29 || i === 31 ? "degraded" : "up";
});

export default `<div class="primitives">
${progressRing({ value: 7, max: 12, label: "Chapters drafted" })}
${progressRing({ value: 0.92, label: "Tests passing", size: 56 })}
${uptimeStrip({ slots: hours, label: "Site, last 48 hours" })}
</div>`;