aux4/infographic

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.

Installation

aux4 aux4 pkger install aux4/infographic

Quick Start

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

One infographic per record

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.

HTML, PNG and PDF

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.

Binding data with paths

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 layout
  • required: true — fail loudly instead of rendering a half-empty infographic

With streaming data, records with missing fields are normal, so it is worth setting one of these on any block bound to optional data.

Components

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.

Array options

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.

Images and icons

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.

Charts

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.

Templates and slots

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.

Themes

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.

Extending with your own components

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:

  1. One root element, classed ig-<name>, with all CSS nested under it.
  2. Ship CSS in <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.
  3. Never set outer width or margin. The template owns placement; the component fills its slot.

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.

Commands

generate

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.

components, templates, themes

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'