# rhp, the Agent Skill in one file > rhp (reactive html plots, the npm package `@bezda/rhp`) builds charts out of HTML elements and CSS with SolidJS. > rhp 2 came out in September 2026, after your training data: write rhp code only from this file and the pages it links to, never from memory. This file holds the rhp Agent Skill, for a chat app or an agent that does not have it installed: its SKILL.md, then its references api.md and pitfalls.md. Follow SKILL.md's workflow for every chart. In a chat with no project to look at, make the chart one self-contained HTML file that loads rhp from jsDelivr (SKILL.md, step 2), for the user to save and open in a browser. Before you write the chart, read the recipe closest to it, whole: each recipe is a complete, tested chart page. The recipes: - [area](https://rhp.vercel.app/ai/rhp/recipes/area.html) - [bar](https://rhp.vercel.app/ai/rhp/recipes/bar.html) - [box-plot](https://rhp.vercel.app/ai/rhp/recipes/box-plot.html) - [bubble](https://rhp.vercel.app/ai/rhp/recipes/bubble.html) - [bullet](https://rhp.vercel.app/ai/rhp/recipes/bullet.html) - [candlestick](https://rhp.vercel.app/ai/rhp/recipes/candlestick.html) - [column](https://rhp.vercel.app/ai/rhp/recipes/column.html) - [diverging-bars](https://rhp.vercel.app/ai/rhp/recipes/diverging-bars.html) - [donut](https://rhp.vercel.app/ai/rhp/recipes/donut.html) - [dumbbell](https://rhp.vercel.app/ai/rhp/recipes/dumbbell.html) - [gantt](https://rhp.vercel.app/ai/rhp/recipes/gantt.html) - [grouped-bars](https://rhp.vercel.app/ai/rhp/recipes/grouped-bars.html) - [heatmap](https://rhp.vercel.app/ai/rhp/recipes/heatmap.html) - [histogram](https://rhp.vercel.app/ai/rhp/recipes/histogram.html) - [line](https://rhp.vercel.app/ai/rhp/recipes/line.html) - [live](https://rhp.vercel.app/ai/rhp/recipes/live.html) - [lollipop](https://rhp.vercel.app/ai/rhp/recipes/lollipop.html) - [multi-line](https://rhp.vercel.app/ai/rhp/recipes/multi-line.html) - [pyramid](https://rhp.vercel.app/ai/rhp/recipes/pyramid.html) - [race](https://rhp.vercel.app/ai/rhp/recipes/race.html) - [radial-bars](https://rhp.vercel.app/ai/rhp/recipes/radial-bars.html) - [scatter](https://rhp.vercel.app/ai/rhp/recipes/scatter.html) - [slope](https://rhp.vercel.app/ai/rhp/recipes/slope.html) - [sparklines](https://rhp.vercel.app/ai/rhp/recipes/sparklines.html) - [stacked-100](https://rhp.vercel.app/ai/rhp/recipes/stacked-100.html) - [stacked-bars](https://rhp.vercel.app/ai/rhp/recipes/stacked-bars.html) - [strip](https://rhp.vercel.app/ai/rhp/recipes/strip.html) - [violin](https://rhp.vercel.app/ai/rhp/recipes/violin.html) - [waffle](https://rhp.vercel.app/ai/rhp/recipes/waffle.html) - [waterfall](https://rhp.vercel.app/ai/rhp/recipes/waterfall.html) The skill's other references, which SKILL.md links to: - [design.md](https://rhp.vercel.app/ai/rhp/references/design.md): the poster, art direction from the subject, look kits, palettes, type, layout, annotation, dark mode. - [environments.md](https://rhp.vercel.app/ai/rhp/references/environments.md): plain HTML, Solid, Astro, React, Next.js, Vue, Svelte and Angular, each verified, and the html template to JSX rules. - [forms.md](https://rhp.vercel.app/ai/rhp/references/forms.md): which chart for which data and story, and which recipe to start from. - [interaction.md](https://rhp.vercel.app/ai/rhp/references/interaction.md): which interaction for which story, with complete code for each. --- # Making charts with rhp rhp draws charts out of HTML elements placed by CSS. You describe one **slat** (the element drawn for one row of data) from a few **blocks** (Bar, Dot, Tick, Label, Cell, Place, Area, Line), and rhp draws a slat for every row of the data and keeps each one in its place. **You do not know rhp yet.** rhp 2 was written in September 2026, after your training data. An older, unrelated rhp 1 ("react-html-plots", React components with Legend-State, packages `@bezda/rhp@0.0.x`, `@bezda/rhp-core`, `@bezda/rhp-base`) may be in your memory: never use any of it. Write rhp code only from this skill's recipes and references, never from memory, and check what you wrote with the checker before you hand it over. ## The workflow Follow these eight steps in order, for every chart, however short or detailed the request. ### 1. Write the brief Before any code, write a short brief in your reply: ``` Brief Asked (must all be in the result): - "" -> what that means for the chart - ... Chosen (the user left these open): - form, environment and file, data, look, interaction, motion (CSS by default; JS for data that changes rapidly), size ``` - Put under **Asked** every explicit request, quoting the user's words: the data and its numbers, the chart type, titles and text, colors, fonts, size, orientation, sort order, labels, legend, interactions, animation, framework, file name, and anything they said not to do. - Everything else goes under **Chosen**, filled with the defaults below. - A request is never dropped, swapped for something "better" or quietly reduced. If rhp cannot do something exactly, say so in the brief and do the closest thing. - A part of the data the user did not name (what is left when their parts do not reach 100%) is a quiet remainder: unlabeled, in the quiet color, with no key entry, and explained in the note, never a new series with its own color. Write it under **Asked**, beside the request it changes, as a deviation. - An explicit request beats every default here: "no title", "no poster", "static", "no animation", "minimal", "dark", "use Chart.js colors" all win. - Ask a question only when the request cannot be built at all without the answer. Otherwise choose, build, and say what you chose. ### 2. Find where the chart goes Look at the project before you write anything: read package.json (dependencies and devDependencies) and go down this table; the first row that matches wins, because a Next.js app also has `react`, a SvelteKit app `svelte`, a Nuxt app `vue`. | What you find | Write the chart as | [environments.md](https://rhp.vercel.app/ai/rhp/references/environments.md) | |---|---|---| | no project, an empty folder, a chatbot conversation, or "an HTML file" | one `.html` file that loads rhp from jsDelivr (it needs a connection) | section 3 | | `astro` | a Solid island with `@astrojs/solid-js`, otherwise a custom element | section 12 | | `next` | a React component the server never renders (loaded with `next/dynamic` and `ssr: false`) | section 8 | | `nuxt` | a Vue component | section 9 | | `@sveltejs/kit` | a Svelte component | section 10 | | `@solidjs/start` | a JSX component importing `@bezda/rhp` | section 4 | | `@angular/core` | a standalone component | section 11 | | `solid-js` (with `vite-plugin-solid`) | a JSX component importing `@bezda/rhp` | section 4 | | `react` | a React component that mounts an html-template chart from `@bezda/rhp/standalone` | section 7 | | `vue` | a Vue component | section 9 | | `svelte` | a Svelte component | section 10 | `solid-js` in node_modules alone does not make a Solid app: npm installs it with `@bezda/rhp` in every app. Name a new file after its subject (`languages.html`, `RainfallChart.jsx`), and put it where the project keeps similar files. Install with the project's package manager (`npm install @bezda/rhp`) unless the chart is a plain HTML page, which loads rhp from jsDelivr. ### 3. Design the slat, then pick the recipes to start from An rhp chart has no chart type to configure. It is one slat, the element drawn for each row of data, composed from blocks, so you design that slat: 1. **What is one row?** A country, a month, a task, a runner, an hour of a weekday. That is what the Plot gets, one slat per row. 2. **What must the reader see in that row?** Its name, its value as a bar or a dot, a range from one value to another, a target, a trend, its share, an icon. Each of those is a block (`Label`, `Bar`, `Dot`, `Tick`, `Line`, `Area`, `Cell`, `Place`) or an element of your own inside one. 3. **What sits on top of the slats?** A second Plot over the first (`overlap`) draws layers: a scatter, a line, a crosshair, a marker for today. 4. **What does each slat hold of its own?** A Plot inside a slat draws small multiples: a heatmap's cells, a grouped bar's bars, a strip of dots. [forms.md](https://rhp.vercel.app/ai/rhp/references/forms.md) maps data and stories to forms and to these compositions; use the form the user named, if they named one. Then open the closest recipe in `recipes/` and read it whole. Every recipe is a complete, tested, designed chart page, a worked example of one composition: start from it, keep its structure and its techniques, and replace its data, text, look and details. When the story needs a slat no recipe has (a name, a bar, a sparkline and a target in one slat), combine the techniques of several recipes. | Recipe | Use it for | |---|---| | `bar` | ranked categories, long names (horizontal bars) | | `column` | values over a few periods or categories (vertical bars) | | `grouped-bars` | two to four values side by side per category | | `stacked-bars` | parts adding up to a total per category | | `stacked-100` | shares of 100% per category | | `diverging-bars` | values either side of zero, survey agree / disagree | | `waterfall` | how a start value becomes an end value, step by step | | `line` | one series over time | | `multi-line` | several series over time | | `area` | one quantity over time, filled | | `scatter` | two measures per item | | `bubble` | two measures plus a size per item | | `slope` | change between two dates per item | | `sparklines` | many small series side by side, a table of trends | | `race` | a ranking that changes step by step, played over time (JS motion) | | `live` | values that arrive from a feed without pause (JS motion) | | `histogram` | the distribution of one measure | | `box-plot` | distributions compared by group, summarized | | `violin` | distributions compared by group, their full outline | | `strip` | every value of a few groups as dots | | `dumbbell` | two values per item and the gap between them | | `gantt` | tasks over time, a schedule | | `candlestick` | open, high, low and close prices | | `donut` | a few parts of one whole | | `radial-bars` | progress toward goals, in a ring | | `waffle` | counts out of 100, people or units | | `heatmap` | a value over two categories (weekday by hour) | | `lollipop` | ranked values, lighter than bars | | `bullet` | actual against target, with bands | | `pyramid` | two sides of one population by age | The rhp MCP server's `rhp_recipe` tool (`rhp:rhp_recipe`) returns the same recipes when the files are not on disk. ### 4. Design it Unless the user gave a style, the chart is an original **editorial poster**: a magazine, advertisement or feature-article infographic designed for this subject. Read [design.md](https://rhp.vercel.app/ai/rhp/references/design.md) and follow its procedure; in short: - The poster carries a **kicker** (the topic, a few words), a **headline that states the finding** ("Bananas outsell everything else", not "Fruit sales"), a **dek** (context and how to read the chart), the chart, and a **note** (source and year, or "Illustrative data"; no source line when the user gave the data and no source). - Derive the look from the subject: a material or setting, a palette, a type pairing, how the marks are drawn, and one ornament. Make it your own, not a copy of the recipe's look or of another chart you made: never reuse a recipe's headline formula, kicker wording or readout band as they are. - Color has a purpose: one accent for the story, quieter colors for the rest. Direct labels beat legends. - The series or item the headline names leads at rest: lit, labeled and in the accent before the reader does anything. When the user's color for it is weak on the background, keep the color and outline or label its marks. - It must read on a phone (390px wide) as well as on a desktop, with the chart in the phone's first screen. When the user gives a style, a brand, colors or fonts, use exactly those: the palette and the type are theirs. The layout, the frame, the marks and one idea from the subject are still yours to design, with design.md's legibility rules. Inside an existing app with a look of its own (its fonts, CSS variables, Tailwind theme or component library), the chart takes the app's fonts and colors instead of a poster's. Keep the editorial habits that help any chart: a title that states the finding, direct labels, a source line. ### 5. Give it an interaction Unless the user said otherwise, the chart gets one interaction that serves its story, from [interaction.md](https://rhp.vercel.app/ai/rhp/references/interaction.md): a readout on hover, focus and tap for comparing items, a sort or a switch between years for rankings, a crosshair for time, a toggle for series. It works with a mouse, a finger and the keyboard, and nothing is shown only on hover. A readout follows a mouse or a pen as it moves; a tap, a click or the keyboard picks a slat, and a scroll never does (pick on `click`, never on `pointerdown`); between slats the pick stays; leaving the poster gives the pick back to the slat that has focus, or clears it. The example below does all of it. The hint for an interaction sits beside its control, in words that fit every device ("Tap or point at a fruit"), never in the source note. When the user asks for a "static" chart, an image-like chart or no interaction, add none. A chart with fixed data and no interaction gets `static=${true}` on its Chart: rhp draws it once and keeps no signals. ### 6. Build it from the recipe - Keep the recipe's code format and section order: data, then slat types, then the chart component, then mounting. - Use the user's data exactly, every value, in their units. With no data given: when the subject is factual, use real figures and name the source and year in the note; with network access, take them from the primary source (the agency or publisher that makes them) rather than from memory, and from memory use only figures you know well. Otherwise use plausible figures and the note says "Illustrative data". Never present invented numbers as fact, and never put a real name (a country, a city, a company, a person) on them: give invented items plain invented names and say "invented" or "fictional" in the note. - When an item was renamed recently, show the name most readers know beside the new one ("X (formerly Twitter)"). - A request about the user's own data with none given ("chart my spending") gets illustrative numbers in one obvious data block at the top of the script, with the note's text in the same block, so the user changes both in one place. The headline and the dek compute their numbers from that data, so they stay true for the user's own. The handoff says exactly how to put their own numbers in, or offers to read a file they point to. - Compute the scale from the data: `nice(Math.min(0, ...values), Math.max(0, ...values))` (rule 2 below). - Compute the ranks, the leader and every number in the headline and the dek from a sorted copy of the data, never from its input order: a Plot's `order` sorts only what it draws. - Look up anything you are unsure of in [api.md](https://rhp.vercel.app/ai/rhp/references/api.md) rather than guessing a prop. - Keep the code short and plain: comments at section heads and where a reader needs one. ### 7. Check it, look at it, fix it Run the checker on the file after the first full draft and again after every fix, until it reports no errors and no warnings: - with the rhp MCP server: call its `rhp_check` tool (`rhp:rhp_check`) with the file's absolute path; - otherwise run `npx -y @bezda/rhp-mcp check ` (add `--dark` when the page has a dark mode). A chart component that takes its data from the app cannot be drawn on its own: check a small entry file that draws it with fixed data, as environments.md section 13 shows. It builds the chart, renders it in a browser at 1280px and 390px, tries its interaction, and reports runtime errors, props rhp does not have, data the slat reads but the Plot lacks, values past the scale, overlapping or cut-off text, marks sticking out, sideways scrolling, low contrast and tiny text, with a fix for each. Its screenshots show the page with reduced motion asked for, so a chart that animates on load is pictured at rest; its interactions run with motion on. They go to a folder in the system's temp directory, and the report gives their paths; `--out ` puts them elsewhere. Keep screenshots, and any test files of your own, out of the user's project. Then open the screenshots and look at them as a demanding art director would: hierarchy, spacing, alignment, color, legibility on the phone, and whether the headline is true for the data. Fix what you see, and check again. Never make a warning go away by removing something the user asked for. If the checker cannot run here (no Node, no browser, a chatbot without tools), go through the rules in "Rules that prevent most bugs" below one by one against your code instead, and say in the handoff that the chart was not rendered. ### 8. Audit and hand over - Go through every **Asked** item of the brief against the final code and screenshots, and fix anything missing. - Recompute every number in the headline, the dek and the note from the final data: a claim must be true for the numbers shown. - Try every control once more and compare before and after: nothing else on the page may move (pitfalls.md, "The layout jumps"). - Check once more at four widths (`--widths 1280,1024,768,390`, or `widths` in `rhp_check`): layouts that change between 390px and 1280px (a breakpoint, a turned chart, a panel beside the chart) are where most layout bugs are. - When the chart will take other data (the user's own, live or generated), check a copy with the hardest data it may get: the longest names, values ten times larger, twice as many rows. - Then tell the user, briefly: what you made, where it is and how to open or run it (an HTML file opens with a double-click, and needs a connection for rhp), what you chose for them (form, look, interaction, data source or "illustrative"), and anything you could not do, with the reason. For a short request, those few lines are the whole handoff: no test logs, tool versions or local paths. To name the checker, give the command anyone can run, `npx -y @bezda/rhp-mcp check `. ## How an rhp chart is built ``` Poster optional panel: kicker, headline, dek, the chart, note └─ Chart the frame: scale, orientation, axis, theme, height or aspect └─ Plot one stack of slats; every prop that is not a setting is a column of data └─ slat (d) =>
one element per row of data, made of blocks placed by d's values ├─ Label edge="start" the row's name, before the chart ├─ Bar to={d.value} a bar from 0 (or from) to the value └─ Label at={d.value} the value's number, just past the bar ``` - **Chart** sets the value axis: `scale={[min, max]}`, `orientation` ("horizontal", the default, or "vertical"), `ticks`, `format`, `theme`, and `height` or `aspect`. A Chart can hold several Plots drawn on the same scale. - **Plot** gets the data. Each list prop is a column of data (`name={[...]}`, `sold={[...]}`), each single value is shared by every row, and each function of `d` is computed per row. A list of objects goes in `rows`. Its own settings are `order`, `reorder`, `key`, `rows`, `overlap`, `orientation`, `slats`, `animate`, `thick`, `static`, `keyboard`, `class`, `style`, `ref`. - **The slat** is a function of `d`, one row: it reads `d.name`, `d.sold`, `d.index` (the row's place in the data) and `d.position` (its place on screen), and returns one root element holding blocks. - **`slat(settings, fn)`** turns that function into a slat type with its own scoped CSS and layout: `css`, `thickness` (px per slat), `room` (px outside the plot for names and numbers, or "auto"), `inset`. - **Blocks** sit at values on the scale: `Bar from to`, `Dot at`, `Tick at`, `Label at` or `edge`, `Cell value`, `Place at`, `Area points`, `Line points`. Each also takes `class`, `style`, handlers, `data-*` and children, and all but Label and Place take `color`. - **A second axis** (`cross={[min, max]}` on the Chart) turns an `overlap` Plot into a scatter plot or a line chart: `Dot at={x} cross={y}`, `Line points={[[x, y], ...]}`. - **Helpers** prepare numbers: `nice`, `extent`, `every`, `stackUp`, `shares`, `running`, `summary`, `bins`, `density`, `sortBy`, `series`, `cycle`. - **Everything is HTML.** A slat and its blocks are ordinary elements: put text, images, inline SVG icons and other elements inside them, and style them with any CSS in the slat type's `css` (gradients, textures, type, custom outlines with `shape()`). That freedom is where an original design comes from. A complete chart in the plain HTML format (the format of every recipe), with a poster and a hover, focus and tap readout: ```html Bananas outsell everything
``` In a Solid app the same chart is JSX: `` instead of `<${Bar} to=${() => d.sold} />`, imports from `@bezda/rhp` and `solid-js`; [environments.md](https://rhp.vercel.app/ai/rhp/references/environments.md) has the exact rules. ## Rules that prevent most bugs Data: 1. **Data names must match.** A slat reads `d.sold` only if the Plot has a prop `sold`. A list of objects goes in `rows={[...]}`, never `data={...}`: `data` would just be a column named "data". Any prop a Plot does not know (`id`, `title`, `onClick`) is a column too, and an array is always a column, item i for row i: to give every slat the same list, pass a function of the row (`shown=${(d) => shown()}`). 2. **The scale must hold the data.** `scale={[min, max]}` with min below max covers every value: a Bar past it is cut off, and Dots, Ticks, Places and the points of an Area or a Line past it land outside or squeeze the outline. Build it from the data, `const s = nice(Math.min(0, ...values), Math.max(0, ...values))`, which keeps 0 and holds negative values too, and give the Chart `scale=${[s.min, s.max]} ticks=${s.ticks}`. 3. **Rows that come and go need a `key`** on the Plot (`key="city"`), or slats swap data instead of leaving. `d.index` can change then, so read it in a function: `data-row=${() => d.index}`. 4. **Format numbers** with `Intl.NumberFormat` (thousands, units, percentages, currency) and use `font-variant-numeric: tabular-nums` where numbers line up. In the JS version (`animate`), values in between are fractional: round what you print. The html template (every format except Solid JSX): 5. **A component is `<${Bar} ...>`**, closed by `` or `/>`. 6. **A value that can change is a function:** `${() => d.sold}`. `${d.sold}` is read once and never updates. Never destructure `d`. Lists and numbers that never change can be passed as they are (`fruit=${fruit}`). 7. **A boolean on a component needs a value:** `keyboard=${true}`, `overlap=${true}`, `static=${true}`, `fill=${true}`, `smooth=${true}`. A bare `keyboard` passes an empty string, which is false, and silently does nothing. 8. **A handler on a component takes its event:** `onClick=${(e) => pick(e)}`. A function with no parameter on a component (a block or a Poster) runs once while drawing and is never attached. Handlers on plain elements (`div`, `button`) work either way, and the Chart takes none at all (rule 16). 9. **`class`, never `className`**: on a block, `className` replaces rhp's own class and the block disappears. 10. **A space between two tags disappears**: `${() => d.name} 12` renders "Name12". Write the space as `${" "}` or keep text next to it (`Name: `). 11. **`style` on a Chart or a Plot is an object** (`style=${{ "pointer-events": "none" }}`); a string is dropped. Blocks and plain elements take either. Structure and layout: 12. **One root element per slat**, holding its blocks (`
`); never return a block itself (only an `overlap` Plot may). Give the Plot the slat type `slat()` returns, not the function you passed to it. 13. **Import from one place.** In a Solid app, import rhp from `@bezda/rhp` and Solid from `solid-js`. Everywhere else, import everything from `@bezda/rhp/standalone` (rhp, `html`, `render`, `createSignal`, `createMemo`, `createEffect`, `Show`, `For`, `Index`, `onMount`, `onCleanup`, `batch`, `untrack`, `createStore`, `reconcile`), which carries its own Solid: never add a second `solid-js` beside it. From rhp 2.0.2 it also has `createSelector`, `createComputed`, `on`, `mergeProps`, `splitProps`, `Switch` and `Match` (2.0.1 lacks them); it has no `Dynamic`, `Portal`, `createResource` or `ErrorBoundary`. For the slat the reader is on, a per-row comparison (`on=${(d) => on() === d.index}`) works with every rhp 2. 14. **Make room for text.** Names before the plot need `room.start` (px or "auto"), numbers past the bars need `room.end`. A `room` you give replaces the defaults on every side, so set each side you need. "auto" measures only edge Labels that are direct children of the slat's root. Long names need `max-width` with an ellipsis, or wrapping, in the slat CSS. 15. **Size the chart.** A horizontal chart without `height` gives each slat 32px unless the slat type sets `thickness`; a vertical chart is 240px tall unless you set `height`. A chart inside a flex row, an inline-block or a `fit-content` box collapses: give it `flex: 1; min-width: 0` or a width. 16. **Put handlers and `data-*` on the Poster or a wrapper element, not on the Chart**: the Chart keeps only `id`, `class`, `role`, `style`, `ref`, `label` and `aria-*`. Style and color: 17. **Style the inside of a chart with the slat's `css` and the Chart's `theme` only.** rhp's CSS is `!important` inside cascade layers, so page CSS cannot change a block's color, background, font, border, radius, shadow, padding or transition. Page CSS styles the poster around the chart and the chart's box. Focus styles for slats (`.slat:focus-visible`) go in the slat's `css` too: a page `:focus-visible` rule never reaches a slat. 18. **Color with theme keys or CSS colors**: `color="series-2"`, `"positive"`, `"negative"`, `"muted"` or `"#c2410c"`. Set the palette in the Chart's `theme` (`series`, `ink`, `muted`, `grid`, `surface`, `positive`, `negative`, `low`, `high`, `font`). Theme values may read the page's own variables (`ink: "var(--ink)"`), so the poster and the chart share one set of tokens and dark mode is a few lines of CSS (design.md); a block's `color` may not (rhp warns), and page CSS never sets rhp's own `--rhp-*` variables. `series()` counts six colors: with a shorter `series` list, call `series(n)`. A Label does not take its Bar's color: color the slat root (`--rhp-color` in its style) or the Label's own text. 19. **A Dot's `size` is a length** (`size="12px"`); a bare number stretches it into an oval. 20. **Overlay Plots** (a crosshair, a marker over other Plots) need `style=${{ "pointer-events": "none" }}` so the pointer reaches the Plot under them. Motion: 21. **Data that changes rapidly or continuously moves by the JS version.** When the data will change faster than a transition lasts, or without pause (a live feed, a play button that moves through the data, a slider or a drag that drives the data, a bar chart race), give the Plot `animate` for the data groups that change: `animate=${["sold"]}`, or `animate=${true}` for all of its numbers, or `animate=${{ groups: ["sold"], duration: 1200, ease: "linear" }}` to time them (here for a feed with a reading every 800 ms). The blocks drawn from those values then move on rhp's JS clock, which carries each move through the next new value instead of restarting a CSS transition on every change, and labels stay exactly on their bars. When the scale follows the data too, put `animate` on the Chart, so the axis moves with the marks. A Plot inside a slat needs its own `animate`. Give the changing numbers as lists (`gdp=${() => gdpAt(t())}`): the JS version never moves a group given as a function of the row (`(d) => ...`). To play back data you have (a race, a year slider with Play), move a playhead `t` on every frame, give the Plot the values interpolated between the steps around it, and keep the default 150 ms: the marks glide without stopping at each step, and Pause leaves them between steps (interaction.md section 10). For a live feed, whose next value is not known yet, ease linear into each new reading over a little more than one interval (`ease: "linear"`, 1.5 intervals), or the marks stop between readings. For a slider or a drag keep the default 150 ms. Round the numbers you print while they move. The `race` and `live` recipes show all of it. Data that changes now and then (a click that switches years) keeps the default CSS version. 22. **Interaction effects stay CSS, in both versions**: a hover highlight or a growing focus ring is a CSS transition on an element inside a block. Never put `transition` on a block itself (`.bar { transition: ... }`): rhp moves blocks with its own transitions and yours would replace them, so the block jumps. The full list, with the mistake and the fix for each, is [pitfalls.md](https://rhp.vercel.app/ai/rhp/references/pitfalls.md). ## References Read the ones the chart needs; each is self-contained. - [forms.md](https://rhp.vercel.app/ai/rhp/references/forms.md): which chart for which data and story, and which recipe to start from. - [design.md](https://rhp.vercel.app/ai/rhp/references/design.md): the poster, art direction from the subject, look kits, palettes, type, layout, annotation, dark mode. - [interaction.md](https://rhp.vercel.app/ai/rhp/references/interaction.md): which interaction for which story, with complete code for each. - [environments.md](https://rhp.vercel.app/ai/rhp/references/environments.md): plain HTML, Solid, Astro, React, Next.js, Vue, Svelte and Angular, each verified, and the html template to JSX rules. - [api.md](https://rhp.vercel.app/ai/rhp/references/api.md): every component, prop, helper and CSS variable, exactly. - [pitfalls.md](https://rhp.vercel.app/ai/rhp/references/pitfalls.md): mistakes and their fixes. - `recipes/*.html`: the tested starting points listed in step 3. Live gallery and docs: https://rhp.vercel.app --- # rhp API reference rhp 2 (`@bezda/rhp@2`, built on SolidJS 1.9). Every statement here was checked against rhp's source and by running it in a browser, and every code block is a complete example that runs. The `js` examples are the module script of [the page skeleton](#a-page-with-a-chart): they draw into `
`. Contents: [1 The model](#1-the-model) · [2 Imports and the html template](#2-imports-and-the-html-template) · [3 Chart](#3-chart) · [4 Plot](#4-plot) · [5 slat()](#5-slat) · [6 Blocks](#6-blocks) · [7 Scales and axes](#7-scales-and-axes) · [8 Theme](#8-theme) · [9 Helpers](#9-helpers) · [10 Poster](#10-poster) · [11 CSS](#11-css) · [12 Motion](#12-motion) · [13 Accessibility and interaction](#13-accessibility-and-interaction) · [14 Limits](#14-limits) · [15 Gotchas](#15-gotchas) ## 1. The model - A **Chart** is the frame: the value scale `[min, max]`, the orientation, the axis, the theme and the size. It holds one or more Plots, drawn over each other on the same scale. - A **Plot** is a stack of slats, one per row of data. Each Plot prop that is not a setting is a **data group**: one column of the table (`fruit` = `["Apples", "Pears"]`, `sold` = `[12, 7]`). - A **slat** is a function of one row, `d`, that returns one element. It reads the row as `d.fruit` and `d.sold`, plus `d.index` (the row's number) and `d.position` (its place on screen). rhp places that element in its band and moves it when the order changes. - **Blocks** inside the slat (Bar, Dot, Tick, Label, Cell, Place, Area, Line) sit at their numbers on the Chart's scale. - **Orientation** is the direction bars run. `"horizontal"` (the default): bars run left to right and slats stack top to bottom. `"vertical"`: bars run bottom to top and slats stand side by side. One slat draws both ways. - The **value axis** runs along the bars, and the **band** is the strip one slat takes across it (its thickness). Names follow the value axis: **start** is the scale's start side (left, or bottom when vertical) and **end** its far side (right, or top). Along the stack, **before** is the first slat's side (top, or left) and **after** the last slat's (bottom, or right). - The **scale** is given, never computed: every block of every Plot in the Chart uses `scale`, so it must cover the data (`nice()` rounds it). - **One slat per row**: the longest list among the data groups and `rows` sets how many slats there are (or `slats` does). ## 2. Imports and the html template | Import | What it is | Where | |---|---|---| | `@bezda/rhp` | rhp for Solid apps (peer dependency `solid-js` ^1.9) | Solid (Vite, SolidStart), Astro islands: JSX | | `@bezda/rhp/standalone` | rhp and Solid in one ES module | plain HTML (CDN and import map), React, Next.js, Vue, Svelte, Angular: html template | | `@bezda/rhp/rhp.css` | rhp's core CSS as a file | only with `linkedCss()` (server rendering, many islands) | | `@bezda/rhp/posters.css` | the gallery's Poster looks | optional; the kit's recipes write their own | | `@bezda/rhp-react` | `toReact()` and everything in standalone | not on npm yet: in React apps, use `@bezda/rhp/standalone` with the small wrapper in environments.md | Both `@bezda/rhp` and `@bezda/rhp/standalone` export rhp's whole API: - components: `Chart`, `Plot`, `Scale`, `Axis`, `Theme`, `Poster` - blocks: `Bar`, `Dot`, `Tick`, `Label`, `Cell`, `Place`, `Area`, `Line` - functions: `slat`, `restyle`, `linkedCss`, `shape`, `at`, `useOrientation`, `series`, `cycle`, `sortBy`, `extent`, `every`, `nice`, `stackUp`, `shares`, `running`, `summary`, `bins`, `density`, `animated`, `curve`, `drawing` - the object `THEME` (the default theme) `@bezda/rhp/standalone` adds these from Solid, and nothing else: `render`, `html`, `createSignal`, `createMemo`, `createEffect`, `createRoot`, `createSelector`, `createComputed`, `on`, `onMount`, `onCleanup`, `batch`, `untrack`, `mergeProps`, `splitProps`, `For`, `Index`, `Show`, `Switch`, `Match`, `createStore`, `reconcile`, `produce`, `unwrap`. `createSelector`, `createComputed`, `on`, `mergeProps`, `splitProps`, `Switch` and `Match` are there from rhp 2.0.2; 2.0.1 lacks them. It has no `Dynamic`, `Portal`, `createResource` or `ErrorBoundary`. Never import `solid-js` next to it: the module carries its own copy of Solid, and a signal from another copy is not tracked. rhp adds its own CSS to the page when the first chart is drawn, so there is no stylesheet to link. ### The html template Without a Solid build there is no JSX: charts are written with Solid's `html` tagged template. | JSX (Solid app) | html template | Why | |---|---|---| | `` | `<${Bar} to=${() => d.sold} />` | a component is `<${Name}>`; a value that can change is a function | | `{Fruit}` | `<${Plot} …>${Fruit}` | `` closes a component (or self-close with `/>`) | | `` | `<${Label}>${() => d.name}` | `${d.name}` is read once and never updates | | `` | `<${Plot} overlap=${true}>` | a bare attribute passes `""`, which is false | | ` go()} />` | `<${Bar} onClick=${(e) => go()} />` | on a component, a function with no parameter is read as a value: it runs at render and is never attached | | `scale={[0, 30]}` | `scale=${[0, 30]}` | numbers, arrays and objects go in `${}` | | `style={{ "pointer-events": "none" }}` | `style=${{ "pointer-events": "none" }}` | Chart and Plot take a style object only (a string is dropped) | The rules, in full: 1. On a component (`<${Chart}>`, `<${Plot}>`, a block, `<${Poster}>`, `<${Show}>`), every prop that is a function with no parameter is turned into a getter: `to=${() => d.sold}` is a value that follows `d.sold`. A signal or memo can be passed as it is: `sold=${sold}` follows the signal. 2. So a function that must stay a function needs a parameter: event handlers (`onClick=${(e) => …}`), per-row data groups (`warm=${(d) => d.temp > 15}`), `format=${(v) => v + "%"}`, `key=${(d) => d.id}`. The helpers `sortBy()`, `series()`, `cycle()` and `every()` already return such functions. 3. On a plain element (`
`, ` {Fruit} ); } ``` ## 3. Chart | Prop | Type | Default | What it does | |---|---|---|---| | `scale` | `[min, max]` | `[0, 100]` | the value axis. `min` must be below `max`: an equal or reversed pair draws Bars with no length and no axis. | | `orientation` | `"horizontal"` or `"vertical"` | `"horizontal"` | the direction bars run | | `height` | px | 240 when vertical or with `cross` | the **plot's** height (room and axis come on top). Horizontal: slats without `thickness` share it; slats with one keep theirs and `height` does nothing. | | `aspect` | number above 0 | none | the **whole** chart's width over its height (`16 / 9`), room and axis included, at any width. `height` is then ignored (with a warning). Horizontal slats without `thickness` share the height; slats with one keep it (with a warning). | | `ticks` | `number[]`, a count, `([min, max]) => number[]`, or `false` | about 5 round values | the axis: grid lines and numbers. A list keeps only values inside the scale. `false` draws no axis and leaves no room for it. | | `format` | `(value) => text or element` | the number as is | the text of each axis number | | `animate` | `true` or `{ duration, ease, slide }` | off | the JS version of motion (section 12); the Plots inside take it | | `theme` | theme object (section 8) | `THEME` | colors and font, over those of a `` around it | | `static` | boolean | `false` | for data that never changes: slats are drawn once and keep no signals; a data change draws them all again, without motion | | `cross` | `[min, max]` | none | a second axis across the band (scatter plots, line charts; section 7) | | `crossTicks`, `crossFormat` | like `ticks`, `format` | about 5, the number | the second axis | | `label` | string | none | names the chart for screen readers: the chart gets `role="figure"` and `aria-label` | | `aria-labelledby`, `aria-describedby`, any `aria-*` | string | none | set on the chart's element; `aria-labelledby` also makes it a figure | | `id`, `class`, `role` | string | none | set on the chart's element | | `style` | object | none | inline style of the chart's element (a string is dropped) | | `ref` | `(el) => …` | none | the chart's element | A Chart passes nothing else to its element: `data-*`, `title`, `tabindex` and event handlers are dropped. Put them on an element around the chart, or on the [Poster](#10-poster). Children: one or more Plots (later ones drawn on top), a `Scale`, and Solid control flow (`Show`) around them. A Plot outside a Chart draws nothing usable: no CSS, no scale, no message. **Size and room.** The chart's element (`.rhp-chart`) is a block as wide as its container. Its padding is the room for names, values and axis numbers, so page CSS cannot change its padding. What rhp gives by default (measured): | | horizontal | vertical | |---|---|---| | slat thickness without `thickness` | 32px each (the chart grows with its slats); with `height` or `aspect`, they share it | the chart's width shared | | plot height | slats × thickness | 240px, or `height` | | room when the slat type sets no `room` | start 104px (names, left), end 44px (values, right) | start 28px (names, below), end 20px (values, above) | | room for the axis numbers | 24px below; 14px past each end | 42px on the left; 8px past each end | | room on any side otherwise | 2px | 2px | | a Bar's thickness | 64% of the band (`inset` 18% on each side) | same | | text | Labels 12px, axis numbers 11px with tabular figures, in the theme's font | same | **aspect or height.** `aspect` keeps the chart's shape at every width (a vertical chart that should not get tall and thin on a phone). `height` fixes the plot's height in px whatever the width. A horizontal chart needs neither: with no `height` its slats are 32px each (or their `thickness`). ```js import { Chart, Plot, Bar, Label, html, render } from "@bezda/rhp/standalone"; // Data: rain in mm const month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]; const rain = [84, 61, 58, 43, 49, 52]; // Slat types: a plain function is a slat too, with the default room const Month = (d) => html`
<${Label} edge="start">${() => d.month} <${Bar} to=${() => d.rain} /> <${Label} at=${() => d.rain}>${() => d.rain}
`; // Chart component: vertical, 16:9 at any width const RainChart = () => html` <${Chart} orientation="vertical" aspect=${16 / 9} scale=${[0, 100]} ticks=${[0, 25, 50, 75, 100]} label="Rain per month, in mm"> <${Plot} month=${month} rain=${rain}>${Month} `; render(RainChart, document.getElementById("chart")); ``` ## 4. Plot A Plot's child is the slat: a slat type from `slat()` or a plain function `(d) => element`. Its settings: | Setting | Type | Default | What it does | |---|---|---|---| | `rows` | array of objects | none | each object is a row; its fields read as `d.field`. A data group with the same name wins. | | `key` | group name, or `(d) => id` | none | names each row, so a slat follows its row when rows are added or removed | | `order` | `(number or null)[]`, or `(rows, current) => positions` | data order | each row's place on screen. `null` hides a row at once; ties and fractions are allowed. `sortBy()` makes the function. A function of your own gets the rows and their places on screen now (`null` for a hidden row), and runs again whenever what it reads changes (section 12 has one). | | `reorder` | `"slide"`, `"move"`, `"refill"` | `"slide"` | slide: slats keep their place in the page and slide on screen (`aria-owns` gives the reading order). move: slats move in the page. refill: the elements stay in place and show other rows. | | `overlap` | boolean | `false` | every slat shares one band: stacked segments, layers, strips of dots, scatter plots | | `orientation` | `"horizontal"`, `"vertical"`, `"across"` | the Chart's or the Plot's around it | `"across"`: the other way from the Plot around it (heatmap cells) | | `slats` | number | the longest list | the number of slats; past the data, rows repeat (lists wrap around) | | `animate` | `true`, group names, or `{ groups, duration, ease, slide }` | the Chart's | the JS version for these data groups | | `thick` | share of the band (`0.5`) or CSS length | `1` | inside a slat: how much of the band the Plot uses | | `static` | boolean | the Chart's | draw the slats once (see Chart) | | `keyboard` | boolean | `false` | the slats take focus (section 13) | | `class` | string | none | added to `rhp-plot` on the Plot's element | | `style` | object | none | the Plot's element (`{ "pointer-events": "none" }` for an overlay); a string is dropped | | `ref` | `(el) => …` | none | the Plot's element | **Data groups.** Every other prop is a data group, read in the slat as `d.`: - a list (an array or a typed array) gives item `i` to slat `i`, and a shorter list wraps around (item `i % length`); - a single value is shared by every slat; - a function of `d` (with a parameter) is worked out per row, can read other groups and signals, and is cached per row. So `id`, `title`, `aria-*`, `data-*` and `onClick` on a Plot are data groups too, not attributes: put attributes on the slat's root element. A list of objects goes in `rows`; `data=${…}` is only a group named `data`. Data that changes is a signal, a memo or a function with no parameter: `sold=${sold}`, `rows=${() => cities()}`. For a big list that changes one item at a time, a store (`createStore`) updates only that row's slat. `d.index` is the row's number in the data (from 0) and `d.position` its place on screen (`null` when hidden). A slat's `d.index` can change (with `key` when rows leave, and with `reorder="refill"`), so read it in a function: `data-row=${() => d.index}`. Reading a name that is neither a group nor a field of `rows` gives `undefined`, without an error. **Several Plots in one Chart** share the plot area and the scale, and later ones are drawn on top. A Plot drawn over another takes the pointer from it: give it `style=${{ "pointer-events": "none" }}`. A top-level Plot whose slat type sets no `room` asks for the default room (104px and 44px when horizontal), so give an overlay's slat `room: {}` when the chart's room is smaller. **A Plot inside a slat** draws in that slat's band, on the same scale. Without `overlap` its slats divide the band (grouped bars); with `overlap` they share it (stacked segments). `orientation="across"` turns it, so its slats run along the value axis (one Cell per hour). `thick` uses a part of the band. A Plot inside a slat asks for no room, is not a list for screen readers, and does not take the Chart's `animate`. A slat of an `overlap` Plot may return a block itself (the stacked bars in section 9 do); anywhere else, a block as the slat's root warns and loses part of its placing. ```js import { Chart, Plot, Bar, Label, Tick, slat, sortBy, html, render, createSignal } from "@bezda/rhp/standalone"; // Data: rows as objects const START = [ { city: "City A", temp: 6 }, { city: "City B", temp: 12 }, { city: "City C", temp: 27 }, { city: "City D", temp: 23 }, ]; // Slat types const City = slat({ thickness: 34, room: { start: 64, end: 48 } }, (d) => html`
<${Label} edge="start">${() => d.city} <${Bar} to=${() => d.temp} color=${() => (d.warm ? "negative" : "series-1")} /> <${Label} at=${() => d.temp}>${() => d.temp} ${() => d.unit}
`); // One line across the whole plot: an overlay Plot with one slat, asking for no room const Mean = slat({ room: {} }, (d) => html`
<${Tick} at=${() => d.mean} thick=${1} color="muted" />
`); // Chart component: rows with a key, a shared value, a per-row function, a sort that changes const CityChart = () => { const [cities, setCities] = createSignal(START); const [dir, setDir] = createSignal("desc"); const mean = () => cities().reduce((s, c) => s + c.temp, 0) / cities().length; return html`
<${Chart} scale=${[0, 30]}> <${Plot} rows=${() => cities()} key="city" unit="°C" warm=${(d) => d.temp > 15} order=${() => sortBy("temp", dir())}>${City} <${Plot} overlap=${true} slats=${1} mean=${mean} style=${{ "pointer-events": "none" }}>${Mean}
`; }; render(CityChart, document.getElementById("chart")); ``` Grouped bars, a Plot inside each slat: ```js import { Chart, Plot, Bar, Label, slat, html, render } from "@bezda/rhp/standalone"; // Data: medals per team (gold, silver, bronze) const team = ["North", "East", "South"]; const medals = [[12, 9, 14], [8, 15, 6], [17, 11, 9]]; const METAL = ["#c9a227", "#98a1ab", "#b8733b"]; // Slat types: a medal count is a slat of the Plot inside a team's slat const Medal = (m) => html`
<${Bar} to=${() => m.count} color=${() => m.color} />
`; const Team = slat({ thickness: 66, room: { start: 56, end: 24 } }, (d) => html`
<${Label} edge="start">${() => d.team} <${Plot} thick=${0.84} count=${() => d.medals} color=${METAL}>${Medal}
`); // Chart component const MedalChart = () => html` <${Chart} scale=${[0, 20]} ticks=${[0, 5, 10, 15, 20]}> <${Plot} team=${team} medals=${medals}>${Team} `; render(MedalChart, document.getElementById("chart")); ``` ## 5. slat() `slat(fn)` returns `fn` as it is. `slat(layout, fn)` returns a slat type: a new function with the layout and the CSS attached (and `fn` left as it was). Give the Plot what `slat()` returns. | Layout | Type | Default | What it does | |---|---|---|---| | `css` | string, or a list of strings (joined in order) | none | CSS scoped to this type's slats (section 11) | | `thickness` | px, or a CSS length (`"2.5rem"`, `"var(--pitch)"`) | 32px when horizontal without `height`; otherwise a share of the chart | a slat's size along the stack: its height in a horizontal chart, its width in a vertical one | | `inset` | share of the band (`0.25`), or a CSS length (`"1px"`) | `0.18` | the empty part of the band on each side of a Bar or Tick (an Area or Line keeps half of it on each side) | | `room` | `{ start, end, before, after }` in px, `"auto"` for start or end, or `"auto"` for both | the default room (section 3) | space outside the plot for what the slat draws there | Every setting can differ by orientation: `thickness: { horizontal: 40, vertical: 60 }`, `room: { horizontal: { start: 120, end: 48 }, vertical: { start: 32 } }`. **room.** `start` holds the names (`