Generate professional infographics as HTML from a config file and JSON data.
You describe the infographic once in config.yaml — a template, a theme, and a list of blocks — and then render it against any JSON. Pipe a stream of NDJSON records and you get one infographic per record, which is what makes this different from a design tool: personalised reports at whatever scale your data has.
Components and templates are ordinary aux4 commands, so any package can add new ones and they show up automatically.
aux4 aux4 pkger install aux4/infographic
Describe the infographic in config.yaml:
config:
report:
infographic:
template: portrait
title: "{name} — Q3"
theme:
name: midnight
blocks:
- type: hero
path: $.customer
eyebrow: Quarterly Report
title: $.name
subtitle: $.plan
- type: stats
path: $.summary
label: $.label
value: $.value
trend: $.delta
- type: footer
text: Generated by aux4/infographic
Render it against a record:
echo '{"customer":{"name":"Acme","plan":"Enterprise"},"summary":[{"label":"Calls","value":1284900,"delta":42}]}' \
| aux4 infographic generate --configFile config.yaml --config report --output acme.html
Saved infographic to acme.html
Pipe NDJSON and use {path} placeholders in --output. Each record is rendered independently:
cat customers.ndjson | aux4 infographic generate \
--configFile config.yaml --config report \
--output "out/{id}.html"
Saved infographic to out/acme.html
Saved infographic to out/northwind.html
Generated 2 infographics.
The placeholder is resolved against the record, so {id}, {customer.name} and {region} all work. Slashes in the value are replaced with - so a record can never write outside the output directory.
The generated page is HTML. --format also exports it as an image or a document:
aux4 infographic generate --configFile config.yaml --config report --output acme.png
aux4 infographic generate --configFile config.yaml --config report --output acme.pdf
The format is inferred from the output extension, so --format is only needed when the two disagree. PNG and PDF cannot be written to stdout, so they require --output.
PNG and PDF need aux4/browser, which is checked when you use them rather than installed up front — nobody who only wants HTML should have to install a headless browser. Install it with aux4 aux4 pkger install aux4/browser.
A whole run shares one browser session and navigates between records, so exporting a stream of records costs one browser start rather than one per record. If the browser daemon was not already running it is started and stopped for you. Note that aux4/browser stops its daemon when its last session closes, so an export can leave an already-running daemon stopped.
The image is captured at the template's canvas width, full height. PDFs use A4 for the a4 template and Letter for the rest.
Each block has an optional path that selects the slice of the record the component receives. Inside the block, prop values that start with $ are resolved against that slice:
| Expression | Resolves to | |---|---| | $.name | name on the current slice | | $.items[0].label | first item's label | | $.items[*].label | every item's label | | $ | the whole slice | | $$.period | period on the record root, escaping the slice | | Quarterly Report | a literal — anything not starting with $ |
$$ matters when a block is scoped to a slice but still needs something from the top of the record.
When a path resolves to nothing, the value is empty. Two block options control what happens next:
skipIfEmpty: true — drop the block entirely and close the gap in the layoutrequired: true — fail loudly instead of rendering a half-empty infographicWith streaming data, records with missing fields are normal, so it is worth setting one of these on any block bound to optional data.
Every component takes either an object or an array. Run aux4 infographic components to see the list with the shape each expects.
| Component | Shape | What it renders | |---|---|---| | hero | object | Title block with optional eyebrow, subtitle, badge and logo | | banner | object | Full-width image with an optional caption bar | | section | object | Heading that divides the page into labelled parts | | text | object | One or more paragraphs of prose | | stats | array | Grid of headline figures with optional trend | | cards | array | Grid of cards with title, value and description | | table | array | Rows and columns, with declared or inferred headers | | ranked-list | array | Ranked rows with proportional bars | | progress | array | Bars showing values against their targets | | comparison | array | Two items side by side, leader highlighted | | chart | array | Inline SVG chart rendered by aux4/chart | | callout | object | Highlighted quote or key message | | footer | object | Closing line with an optional source note |
Nothing converts between the two shapes, so point an array component at an array and an object component at an object.
Every array component accepts the same list options:
- type: ranked-list
path: $.endpoints
label: $.path
value: $.hits
max: 5
offset: 0
overflow: "+${n} more endpoints"
max limits how many items are shown and offset skips from the front. Whatever max hides is reported in an overflow note, because silently dropping items from an infographic misrepresents the data to whoever reads it. Set overflow: false to suppress the note, or give it a string with ${n} for the hidden count.
banner renders a masthead image, hero takes an image for a logo, and stats and cards take an icon per item:
- type: banner
image: assets/masthead.png
text: API usage grew 42% this quarter.
ratio: 16 / 6
bleed: true
- type: hero
title: $.name
image: $.customer.logo
- type: stats
path: $.summary
icon: $.icon
label: $.label
value: $.value
stats and cards centre their contents so a large icon sits above the figure. Set align: left on the block to go back to left-aligned items.
An icon is whatever you supply. A path or URL renders as an image; anything else — an emoji, a symbol, a couple of letters — renders as text. No icon library ships with this package, so nothing constrains you to one set.
Local images are embedded as data URIs so the page stays self-contained, and paths resolve from the working directory. Set embed: false on the block to link to the file instead, which is the better choice when you are publishing to the web and want the asset cached separately. Remote URLs are never fetched — they are written into the page as given, so generating a stream of infographics never depends on the network.
Because the image is embedded per file, a large logo across a stream of records multiplies: a 200KB image over 500 records is 100MB of output. Keep banner and logo assets small, or link them.
The chart component delegates to aux4/chart and inlines the resulting SVG, so the page stays a single self-contained file with no scripts:
- type: chart
path: $.monthly
chart: bar
title: Monthly API calls
x: month
y: calls
height: 320
x, y and series are column names in the data, not paths — leave them unprefixed. Chart colours come from the theme automatically; colors overrides them.
The template owns the canvas size and the regions blocks land in. Run aux4 infographic templates to list them.
| Template | Canvas | |---|---| | portrait | 1080x1350, social portrait | | slide | 1920x1080, presentation slide | | a4 | 794x1123 at 96dpi, print | | web | responsive long-scroll page |
Every template has the slots header, main and footer; slide adds left and right, which render as two columns. A block picks one with slot::
- type: hero
slot: header
- type: chart
slot: left
- type: progress
slot: right
A block with no slot goes to main, and a block naming a slot the template does not have falls back to main with a warning rather than failing. That is what keeps a config portable: switch template: slide to template: portrait and the same blocks still render, just in one column.
Empty slots collapse, so a region with nothing in it leaves no gap. A two-column row with only one side filled becomes a single column rather than half an empty page.
Pick a preset and override only what you care about:
theme:
name: midnight
colors:
primary: "#0ea5e9"
accent: "#f59e0b"
font: "Inter, sans-serif"
spacing: comfortable
radius: 14px
Presets are midnight, daylight, rose-pine and forest — aux4 infographic themes lists them. Overridable colours are background, surface, primary, accent, text, muted and border. spacing is compact, comfortable or spacious.
A component is an aux4 command in the infographic:component profile. Any package that depends on aux4/infographic and registers a command there is picked up automatically — there is no manifest to update and no registration step.
The contract is small. The command receives a JSON payload on stdin:
{
"props": { "title": "$.name" },
"data": { "name": "Acme" },
"root": { "name": "Acme" },
"theme": { "colors": { "primary": "#38bdf8" } }
}
props is the block minus the keys the renderer owns — type, path, slot, skipIfEmpty, required, plus blocks and children, which are reserved. A prop with one of those names never reaches the component. data is the slice selected by path, root is the whole record, and theme carries resolved values for components that generate images or SVG.
End the command's help text with Takes an object. or Takes an array. and aux4 infographic components will report the shape alongside the built-in ones.
The command writes an HTML fragment to stdout, following three rules:
ig-<name>, with all CSS nested under it.<style data-component="name"> — the renderer hoists it into the page and emits one copy per component type, however many times the component is used.Style everything with the theme's CSS custom properties rather than hard-coded colours:
--ig-background, --ig-surface, --ig-primary, --ig-accent, --ig-text, --ig-muted, --ig-border, --ig-font, --ig-gap, --ig-pad, --ig-scale, --ig-radius.
Because these are emitted by the template, a component written today respects a theme invented tomorrow without changing any code.
Templates extend the same way through the infographic:template profile. A template command receives { theme, title, slots, blocks, styles } on stdin and writes the complete HTML page. slots holds the rendered blocks grouped by the slot each named, which is what a template with regions places; blocks is the same markup flattened into document order, for a template that has no regions. End the help text with Slots: header, main, footer. to have them listed by aux4 infographic templates.
Both registries are private commands: generate invokes them, and they are hidden from aux4 infographic --help because they are not how people use the package. They stay executable, so running one by hand with a payload is still how you develop a component.
Renders an infographic to HTML.
aux4 infographic generate --configFile config.yaml --config report [--output <file>] [--data <json>]
--output Where to write the HTML, with {path} placeholders resolved per record. Writes to stdout when omitted --data Inline JSON record. Takes precedence over stdin
Streaming more than one record requires --output, since many infographics cannot share one stdout.
List what is available.
aux4 infographic components
aux4 infographic templates
aux4 infographic themes
components and templates read the live registry, so a component or template installed by another package is listed alongside the built-in ones.
These commands build JSON and present it with aux4/render. Add --format json for the raw records instead of the rendered view:
aux4 infographic components --format json | jq -r '.[] | select(.shape == "array") | .name'