# Novem Documentation > Novem is a data-visualisation platform for coders — create plots, documents, e-mails and dashboards through a simple file-and-path API. This file is the complete documentation rendered as Markdown for LLM consumption. The human-facing docs live at https://novem.io/docs. The programmable HTTP API is described separately at https://api.novem.io/llms.txt. ## Contents - **Get Started** - **Basics** - [Introduction](https://novem.io/docs/start/introduction.md) - **Quick Starts** - [Plots](https://novem.io/docs/start/plot.md) - [Documents](https://novem.io/docs/start/doc.md) - [Mails](https://novem.io/docs/start/mail.md) - [Repos](https://novem.io/docs/start/repos.md) - [Jobs](https://novem.io/docs/start/jobs.md) - **Guides** - **Products** - [Plots](https://novem.io/docs/guides/plot.md) - [Documents](https://novem.io/docs/guides/doc.md) - [Mails](https://novem.io/docs/guides/mail.md) - [Repos](https://novem.io/docs/guides/repos.md) - [Jobs](https://novem.io/docs/guides/jobs.md) - [Spaces](https://novem.io/docs/guides/spaces.md) - **Platform** - [API](https://novem.io/docs/guides/api.md) - **Theming** - [Themes](https://novem.io/docs/guides/theme.md) - [Example](https://novem.io/docs/guides/theme/example.md) - **[Libraries](https://novem.io/docs/guides/libraries.md)** - [CLI](https://novem.io/docs/guides/cli.md) - [Plot](https://novem.io/docs/guides/cli/plot.md) - [Mail](https://novem.io/docs/guides/cli/mail.md) - [Python](https://novem.io/docs/guides/python.md) - **Reference** - **API** - [Terminology](https://novem.io/docs/reference/api/terminology.md) - [Authentication](https://novem.io/docs/reference/api/auth.md) - [Tags](https://novem.io/docs/reference/api/tags.md) - [Social](https://novem.io/docs/reference/api/social.md) - [Slicing](https://novem.io/docs/reference/api/slicing.md) - [Vars](https://novem.io/docs/reference/api/vars.md) - [Files](https://novem.io/docs/reference/api/files.md) - [Sharing](https://novem.io/docs/reference/api/sharing.md) - [Groups](https://novem.io/docs/reference/api/groups_and_orgs.md) - **[Markdown](https://novem.io/docs/reference/markdown/overview.md)** - [Sections](https://novem.io/docs/reference/markdown/sections.md) - [Paragraph](https://novem.io/docs/reference/markdown/paragraph.md) - [Callouts](https://novem.io/docs/reference/markdown/callout.md) - [Visualisation](https://novem.io/docs/reference/markdown/visualisations.md) - [Images](https://novem.io/docs/reference/markdown/img.md) - [Preview](https://novem.io/docs/reference/markdown/preview.md) - [Authors](https://novem.io/docs/reference/markdown/author.md) - [Pages](https://novem.io/docs/reference/markdown/page-breaks.md) - [Page references](https://novem.io/docs/reference/markdown/page-refs.md) - [Spans](https://novem.io/docs/reference/markdown/spans.md) - [Variables](https://novem.io/docs/reference/markdown/variables.md) - [HTML](https://novem.io/docs/reference/markdown/html.md) - [Comments](https://novem.io/docs/reference/markdown/comments.md) - **[Plot](https://novem.io/docs/reference/plot/overview.md)** - [Config](https://novem.io/docs/reference/plot/config/overview.md) - [Type](https://novem.io/docs/reference/plot/config/type.md) - [Title](https://novem.io/docs/reference/plot/config/title.md) - [Colors](https://novem.io/docs/reference/plot/config/colors.md) - [Table](https://novem.io/docs/reference/plot/config/table/overview.md) - [Border](https://novem.io/docs/reference/plot/config/table/border.md) - [Text](https://novem.io/docs/reference/plot/config/table/text.md) - [Align](https://novem.io/docs/reference/plot/config/table/align.md) - [Padding](https://novem.io/docs/reference/plot/config/table/padding.md) - [Format](https://novem.io/docs/reference/plot/config/table/format.md) - [Overflow](https://novem.io/docs/reference/plot/config/table/overflow.md) - [Width](https://novem.io/docs/reference/plot/config/table/width.md) - [Merge](https://novem.io/docs/reference/plot/config/table/merge.md) - [Priority](https://novem.io/docs/reference/plot/config/table/priority.md) - [Chart](https://novem.io/docs/reference/plot/config/table/chart.md) - [Axis](https://novem.io/docs/reference/plot/config/axis.md) - [Labels](https://novem.io/docs/reference/plot/config/labels.md) - [Legend](https://novem.io/docs/reference/plot/config/legend.md) - [Margins](https://novem.io/docs/reference/plot/config/margins.md) - [Charts](https://novem.io/docs/reference/plot/charts/overview.md) - [Bar](https://novem.io/docs/reference/plot/charts/bar.md) - [Custom](https://novem.io/docs/reference/plot/charts/custom.md) - [Tables](https://novem.io/docs/reference/plot/tables/overview.md) - [E-Mail Table](https://novem.io/docs/reference/plot/tables/mtable.md) - [Vars](https://novem.io/docs/reference/plot/vars/overview.md) - **Documents** - [Content](https://novem.io/docs/reference/doc/content.md) - [API](https://novem.io/docs/reference/doc/api.md) - **[Mails](https://novem.io/docs/reference/mail/overview.md)** - [Content](https://novem.io/docs/reference/mail/content.md) - [Recipients](https://novem.io/docs/reference/mail/recipients.md) - [Config](https://novem.io/docs/reference/mail/config.md) - [Sending](https://novem.io/docs/reference/mail/sending.md) - [Renders](https://novem.io/docs/reference/mail/renders.md) - **Grid** - [Layout](https://novem.io/docs/reference/grid/layout.md) - [API](https://novem.io/docs/reference/grid/api.md) - **Repos** - [API](https://novem.io/docs/reference/repos/api.md) - **[Jobs](https://novem.io/docs/reference/jobs/overview.md)** - [Chains](https://novem.io/docs/reference/jobs/chains.md) - [Schedule](https://novem.io/docs/reference/jobs/schedule.md) - [Config](https://novem.io/docs/reference/jobs/config.md) - [Spaces](https://novem.io/docs/reference/jobs/spaces.md) - [Runs](https://novem.io/docs/reference/jobs/runs.md) - **Spaces** - [API](https://novem.io/docs/reference/spaces/api.md) - **Theme** - [Colors](https://novem.io/docs/reference/theme/colors.md) - [Typography](https://novem.io/docs/reference/theme/typography.md) - [Document](https://novem.io/docs/reference/theme/document.md) - [Charts](https://novem.io/docs/reference/theme/charts.md) - [Org themes](https://novem.io/docs/reference/theme/org-themes.md) - **Python Library** - [Plot](https://novem.io/docs/reference/python/plot.md) - [Mail](https://novem.io/docs/reference/python/mail.md) - [Utilities](https://novem.io/docs/reference/python/utilities.md) - **[Profile](https://novem.io/docs/reference/profile/overview.md)** --- # Get Started ## Getting Started ### 1. Sign up to novem Novem is invite-only for now. Get an invite from an existing user — drop into the [novem community on Discord](https://discord.gg/novem) if you don't know anyone yet. ### 2. View novem on the web and your phone You're reading this on the novem webapp at [novem.io](https://novem.io) — that's also where you'll browse, view, and share your content. On the go, install the [novem iOS app](https://apps.apple.com/us/app/novem/id6753790347) to keep your plots in your pocket. ### 3. Connect your agent to novem We recommend using an AI assistant for getting started with creating content for novem. Connect the novem MCP server (`https://api.novem.io/mcp`) to your tool of choice: **Claude Code** Run this once in your terminal: ```bash claude mcp add novem-mcp --transport http https://api.novem.io/mcp ``` Then run `/mcp` at the start of each session to activate the novem tools. **Claude.ai** 1. In [claude.ai](https://claude.ai), go to **Customize → Connectors** and press the **+** next to "Connectors" to **Add custom connector** — or jump straight to the [**Add custom connector** dialog](https://claude.ai/customize/connectors?modal=add-custom-connector). 2. Paste the URL `https://api.novem.io/mcp` and save. 3. Hit **Connect** and complete the sign-in. **Codex** Add the server, then walk through the sign-in: ```bash codex mcp add novem --url https://api.novem.io/mcp codex mcp login novem ``` **ChatGPT** 1. In [chatgpt.com](https://chatgpt.com), enable developer mode under **Settings → Apps → Advanced settings → Developer mode**. 2. Open [**Settings → Connectors**](https://chatgpt.com/#settings/Connectors) and press **Create app**. 3. Point it at the novem MCP server `https://api.novem.io/mcp` and create the app. **Gemini** Run this once in your terminal: ```bash gemini mcp add -t http novem https://api.novem.io/mcp ``` Gemini CLI will prompt you to authenticate on first use. ### 4. Make your first plot Describe what you want in plain language: > Make a novem chart showing the oil price vs the S\&P 500 year-to-date. Style it like Bloomberg. Highlight the most important political events. Use Yahoo Finance as the source. The AI agent finds the data, writes the code, sets up the schedule, and gives you a link to your live plot. View and share it at [novem.io](https://novem.io) or using the novem iOS app. ### Prefer the terminal? Everything an agent does for you is just files behind an API — and the novem CLI speaks it natively. Install it with [pipx](https://pypa.github.io/pipx/): ```bash username@computer:~/$ pipx install novem username@computer:~/$ _ ``` Then generate and store an API token: ```bash username@computer:~/$ novem --init • novem.io username: • novem.io password: ✓ authenticated ✓ token created ✓ new token "novem-cli-computer-rj8chk2j" saved to ~/.config/novem/novem.conf username@computer:~/$ _ ``` From here, each product has a hands-on quick start — [plots](/docs/start/plot.md), [documents](/docs/start/doc.md), [mails](/docs/start/mail.md), [repos](/docs/start/repos.md) and [jobs](/docs/start/jobs.md) — and the [CLI guide](/docs/guides/cli.md) covers configuration and day-to-day usage. *** ### What's next? - **Iterate.** Ask your agent to change the style, add data, or try a different chart type. - **Share.** Every plot has a public URL. Post it to Discord, embed it in an e-mail or a PDF in your corporate branding. - **Connect.** Bring in your own data, from databases to Excel sheets. - **Automate.** Set up a novem job to keep the plot live and updating. ### Getting help Stuck, curious, or want to show off a plot? Join the [novem community on Discord](https://discord.gg/novem) — that's where users and the novem team hang out. ## Quick Start This guide takes you from nothing to a live, shareable bar chart. By the end you'll have built this: *Embedded visualisation:* `/u/novem_demo/p/en_letter_frequency` ### Prerequisites - A novem account — see the [introduction](/docs/start/introduction.md) if you don't have one yet. - For the CLI steps: the novem CLI installed and authenticated (`pipx install novem && novem --init`, details in the [CLI guide](/docs/guides/cli.md)). - For the API steps: an API token (`novem --init` creates one, or use an existing Bearer token). ### 1. Create a plot A plot is created by claiming a name. Names are yours — they only need to be unique among your own plots. **CLI** ```bash novem -p letter_freq -C ``` **API** ```bash curl -X PUT https://api.novem.io/v1/vis/plots/letter_freq \ -H "Authorization: Bearer " ``` ### 2. Add some data Novem plots eat plain CSV — the first column is the category, the second the value. Save this as `letters.csv` (frequency of letters in English text): **letters.csv** ```text A | 8.167 B | 1.492 C | 2.782 D | 4.253 E | 12.702 F | 2.228 G | 2.015 H | 6.094 I | 6.966 J | 0.153 K | 0.772 L | 4.025 M | 2.406 N | 6.749 O | 7.507 P | 1.929 Q | 0.095 R | 5.987 S | 6.327 T | 9.056 U | 2.758 V | 0.978 W | 2.361 X | 0.15 Y | 1.974 Z | 0.074 ``` Then write it to the plot's `data` endpoint: **CLI** ```bash novem -p letter_freq -w data @letters.csv # or pipe it straight from another tool cat letters.csv | novem -p letter_freq -w data ``` **API** ```bash curl -X POST https://api.novem.io/v1/vis/plots/letter_freq/data \ -H "Authorization: Bearer " \ -H "Content-Type: text/plain" \ --data-binary "@letters.csv" ``` > Everything in novem is a file behind a path — data lives at > `data`, configuration under `config/`. The CLI's > `-w path value` writes to any of them; with curl you PUT or POST > to the same paths. See the [API guide](/docs/guides/api.md) for the > full mental model. ### 3. Make it a bar chart The `config/type` value decides how your data is drawn. Set it to `bar`: **CLI** ```bash novem -p letter_freq -w config/type bar ``` **API** ```bash curl -X POST https://api.novem.io/v1/vis/plots/letter_freq/config/type \ -H "Authorization: Bearer " \ -H "Content-Type: text/plain" \ -d "bar" ``` The same data can be re-drawn as any compatible type later — try `hbar` (horizontal) or `line` once you're done. The [chart reference](/docs/reference/plot/charts/overview.md) lists them all. ### 4. Add a title and caption **CLI** ```bash novem -p letter_freq -w config/title \ "Frequency of letters in the English language" novem -p letter_freq -w config/caption \ "Source: Concise Oxford Dictionary, via Robert Lewand's Cryptological Mathematics." ``` **API** ```bash curl -X POST https://api.novem.io/v1/vis/plots/letter_freq/config/title \ -H "Authorization: Bearer " \ -H "Content-Type: text/plain" \ -d "Frequency of letters in the English language" curl -X POST https://api.novem.io/v1/vis/plots/letter_freq/config/caption \ -H "Authorization: Bearer " \ -H "Content-Type: text/plain" \ -d "Source: Concise Oxford Dictionary, via Robert Lewand's Cryptological Mathematics." ``` ### 5. Look at it Your plot renders everywhere — including your terminal: **CLI** ```bash # ANSI render, right in the terminal novem -p letter_freq -x # the shareable web URL novem -p letter_freq -r url ``` **API** ```bash curl https://api.novem.io/v1/vis/plots/letter_freq/url \ -H "Authorization: Bearer " ``` The URL looks like `https://novem.io/u//p/letter_freq` — open it in a browser for the interactive version. PNG, PDF, CSV and ANSI renders of every plot live under its `files/` path. ### 6. Share it Plots are private until you say otherwise. Make it public: **CLI** ```bash novem -p letter_freq -C -s public ``` **API** ```bash curl -X PUT https://api.novem.io/v1/vis/plots/letter_freq/shared/public \ -H "Authorization: Bearer " ``` Now anyone with the URL can see it — embed it in a [mail](/docs/start/mail.md) or a [document](/docs/start/doc.md), or post the link. ### What's next? - **Style it** — colors, axes, legends and more under [plot config](/docs/reference/plot/config/overview.md). - **Try other chart types** — the [bar chart reference](/docs/reference/plot/charts/bar.md) shows stacked and grouped variants; tables, maps and more in the [chart overview](/docs/reference/plot/charts/overview.md). - **Automate it** — the [jobs quick start](/docs/start/jobs.md) keeps a plot updating on a schedule. - **Skip the typing** — connect an AI agent over MCP (see the [introduction](/docs/start/introduction.md)) and describe the plot you want. ## Quick Start ### Prerequisites Make sure you have a [registered novem account](/docs/start/introduction.md#creating-a-novem-account) and the [novem cli installed](/docs/start/introduction.md#installing-the-novem-command-line-tools). ### Creating a document To create a new novem document, use the novem cli with the create and doc flags (`-C -d `). ```bash # create a novem document called quarterly_report novem -d quarterly_report -C ``` #### Adding content The content of a novem document is written in [novem flavoured markdown](/docs/reference/markdown/overview.md) with extensions for [pages](/docs/reference/markdown/page-breaks.md), [variables](/docs/reference/markdown/variables.md), [spans](/docs/reference/markdown/spans.md) and [sections](/docs/reference/markdown/sections.md). Create a local file with your content and write it to the document: ```bash # write content from a file cat report.md | novem -d quarterly_report ``` #### Content with frontmatter Documents support YAML frontmatter for metadata. Place it at the very beginning of your content between `---` delimiters: ```yaml --- title: Quarterly Report author: Analytics Team date: January 2025 --- # Introduction Your document content starts here. ``` #### Adding pages Split your document into multiple pages using the `{{ page }}` section. Each page can have its own layout: ```yaml # First Page (portrait, default) Content for the first page. {{ page orient: landscape cols: 2 }} ## Second Page (landscape, 2 columns) Content for the second page. {{ page }} ## Third Page (default layout) Content continues here. ``` #### Embedding visualisations Embed novem plots and grids directly in your document: ```yaml {{ vis ref: /u/novem_demo/p/nei_rgn_perf width: 100% align: center include title: true include caption: true }} ``` #### Using variables Reference live values from your visualisations inline: ```markdown Revenue this quarter was {/u/acme/p/revenue/v/total} million, up from {/u/acme/p/revenue/v/prev} last quarter. ``` Variables update automatically when the underlying data changes. #### Sharing Share your document publicly or with specific users and groups: ```bash # share with everyone novem -d quarterly_report -w shared/public true # share with a group novem -d quarterly_report -w shared/+org~analytics true ``` ### Examples #### A simple research note Content of `note.md`: ```yaml --- title: Nordic Labour Markets author: Research Team --- # Overview This note examines unemployment trends across the Nordic countries. {{ vis ref: /u/novem_demo/p/unemployment_nordics width: 100% include title: true include caption: true }} {{ callout type: info }} All figures use the harmonised ILO definition for international comparability. {{ /callout }} {{ page orient: landscape }} ## Regional Breakdown | Country | Rate | Trend | |---------|------|-------| | Norway | 3.2% | Stable | | Denmark | 4.8% | Improving | | Sweden | 7.1% | Improving | | Finland | 6.9% | Stable | ``` ```bash # create, add content and share cat note.md | novem -d nordic_labour -C novem -d nordic_labour -w shared/public true ``` ## Quick Start ### Prerequisites Make sure you have a [registered novem account](/docs/start/introduction.md#creating-a-novem-account) and the [novem cli installed](/docs/start/introduction.md#installing-the-novem-command-line-tools). You're also required to have a [valid and confirmed e-mail](/docs/reference/profile/overview.md#e-mail-addresses) registered for your account. ### Creating an e-mail To create a new novem e-mail, simply use the novem cli with the create and mail flags (`-C -m `) in your preferred terminal. `` represents the identifier for the e-mail and will be used when updating content, recipients, sending etc. ```bash # create a novem e-mail called email_name novem -m -C ``` #### Recipients Novem supports the same set of e-mail recipients as regular email: to, cc and bcc. The from name will always be the `profile name` of the user creating the e-mail and the from address will be `@usr.novem.io`. It's possible to specify that a reply\_to address should be supplied, referring to the user registered e-mail. To set the recipients of an e-mail the `--to, --cc and --bcc` flags can be used. The recipients can be either novem users `@novem_username`, novem groups `+org~group`/`@user~group` or regular email `username@domain.ext`/`User Name`. For details and restrictions regarding sending of novem emails please check out the [recipients page](/docs/reference/mail/recipients.md). ```bash # send e-mail to novem user novem -m --to @novem_username # send e-mail to self novem -m --to me # send e-mail to a novem group novem -m --to +org_name~org_groupname novem -m --to @novem_username~user_groupname # send e-mail to address novem -m --to user@domain.ext novem -m --to "User Name " novem -m --to "User Name ;Other User Name" # send e-mail to address, cc self novem -m --to "User Name " --cc me # send e-mail to address, bcc self novem -m --to "User Name " --bcc me # send e-mail to address, cc address novem -m --to "User Name " --cc other@domain.ext ``` #### Content The content of a novem e-mail is written in [novem flavored markdown](/docs/reference/markdown/overview.md) with a few special instructions for e-mail, in particular [e-mail specific sections](/docs/reference/mail/content.md#sections). More details can be found in the [content](/docs/reference/mail/content.md) section of the e-mail documentation. The easiest way to create content is to create a local text file on your disk containing the information. For this section we assume the content is in a file called `email.md`. We strongly recommend that you start all e-mails with a top level section defining the e-mail [subject and preview text](/docs/reference/mail/content.md#subject-and-preview-text). Once you've written your content you can add it to an email by either writing it to standard in or specifying the novem end point. ```bash # Writing data on standard in cat email.md | novem -m # Specifying a content file novem -m -w content @email.md ``` Sample content of email.md ```yaml {{ preview }} Preview text that shows up in list but not e-mail body. {{ /preview }} Text that shows up in the e-mail body. ``` #### Adding plots To add novem plots to the e-mail just include a special type of section called [vis sections](/docs/reference/markdown/visualisations.md). Novem vis sections use the `{{ vis }}` section syntax: ```yaml Some written text before the novem vis section {{ vis ref: /u/novem_demo/p/example_visits align: center width: 100% }} Some text after novem vis section ``` #### Sending To send an e-mail the user can use the send `-S` or test `-T` flags to the novem command. The `-S` flag will send the e-mail to all users (to, cc, bcc) whilst the `-T` flag will only send an e-mail to the registered e-mail of the current user for testing purposes. ```bash # send e-mail to all registered recipients to, cc, bcc novem -m -S # send e-mail to user registered e-mail, ignore to, cc, bcc novem -m -T # send e-mail to user registered e-mail, ignore to, cc, bcc, --to takes precedence novem -m --to -S ``` ### Examples #### A daily e-mail of webpage visitors Content of `draft.txt`. ```markdown {{ preview }} Small summary that shows in preview but not in the body of the e-mail {{ /preview }} Good morning User, yesterday there were 4,323 unique users who visited your webpage. {{ vis ref: /u/novem_demo/p/example_visits align: center width: 100% }} ``` ```bash # create a new novem e-mail novem -m daily_visitor_mail -C # add me to the "to" recipient list novem -m daily_visitor_mail --to me # write the content of the draft.txt to the mail cat draft.txt | novem -m daily_visitor_mail # send the e-mail novem -m daily_visitor_mail -S # shorthand cat draft.txt | novem -C -m daily_visitor_mail --to me -S ``` ## Quick Start By the end of this guide you'll have a repo whose `latest` image you can run from a [job](/docs/start/jobs.md). ### 1. Create the repo `PUT` `/v1/code/repos/hello_repo` The repo comes up as the `job` build type by default — exactly what we want. ### 2. Register a push key If you haven't already added an SSH key to your profile, do it now (HTTPS works too — see step 3). `PUT` `/v1/admin/keys/laptop` `POST` `/v1/admin/keys/laptop/key` ```text ssh-ed25519 AAAAC3NzaC1lZDI1... you@laptop ``` ### 3. Add some code and push novem builds whatever you push. A minimal program is enough to get a working image — here a single Python file: ```bash git clone https://x-token:$NOVEM_TOKEN@novem.io/u//r/hello_repo.git cd hello_repo cat > main.py <<'EOF' print("Hello from my novem repo!") EOF git add main.py git commit -m "hello" git push origin main ``` The push kicks off a build. novem detects the program, packages it into a container image, and publishes it to your registry. ### 4. Watch the build The build runs asynchronously; follow it on the repo's `log` path. `GET` `/v1/code/repos/hello_repo/log` When it finishes, your image is available as: ```text @/hello_repo:latest ``` > **Note:** `latest` tracks your default branch (`main`). Every subsequent push > rebuilds it, and each branch and tag gets its own label. ### Next steps You now have a runnable image. Head over to the [jobs quick start](/docs/start/jobs.md) to run it — on its own, on a schedule, or as one step in a [chain](/docs/reference/jobs/chains.md). ## Quick Start This guide assumes you have a built repo image. If you don't, run through the [repo quick start](/docs/start/repos.md) first to create `hello_repo`. ### 1. Create the job **CLI** ```bash novem -j hello_job -C ``` **API** ```bash curl -X PUT -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/jobs/hello_job ``` It comes up as the `chains` runtime by default. ### 2. Point it at your repo The chain is a single step: the image built from `hello_repo`. **CLI** ```bash echo "hello_repo" | novem -j hello_job -w config/chains ``` **API** ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: text/plain" \ --data "hello_repo" \ https://api.novem.io/v1/code/jobs/hello_job/config/chains ``` novem resolves the bare name to your image `@/hello_repo:latest`. ### 3. (Optional) set an environment variable Anything under `config/env` is injected into the container at run time. Give each variable a lower-case name. It is exposed inside the job in both lower- and upper-case. **CLI** ```bash printf '%s' "world" | novem -j hello_job -w config/env/greet_target ``` **API** ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: text/plain" \ --data "world" \ https://api.novem.io/v1/code/jobs/hello_job/config/env/greet_target ``` ### 4. Run it **CLI** ```bash novem -j hello_job -R # to pass input files, append one or more filenames, each prefixed with @ novem -j hello_job -R @data.csv # save the run's output files to disk with -o/--output novem -j hello_job -R -o ./out ``` **API** ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: application/json" --data '{}' \ https://api.novem.io/v1/code/jobs/hello_job/data ``` ### 5. Read the output Each run is recorded under `runs` with its own log. The job's own `log` is a shortcut for the most recent one. **CLI** ```bash novem -j hello_job -r log ``` **API** ```bash # list runs, then fetch a run's log curl -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/jobs/hello_job/runs curl -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/jobs/hello_job/runs//log ``` You should see your program's output (`Hello from my novem repo!`). > **Note:** Run logs redact the *values* of your environment variables. A > variable printed by your code shows up as `***`. That's intentional, so > secrets don't leak into logs. ### Next steps - [Chains](/docs/reference/jobs/chains.md) — pipe several repo images into a pipeline. - [Schedule](/docs/reference/jobs/schedule.md) — run the job automatically on a cron schedule. - [Runs](/docs/reference/jobs/runs.md) — inspect a run's log, output, stats and status. --- # Guides ## Plots On the novem platform a plot represents the most basic building block of any visualisation. Whilst most plots will probably be charts, several other visual representation exists such as video feeds, terminal windows, browser views, tables, calendars, logs etc. For ease of navigation we've split the novem plot documentation into a few key categories: - Charts - Tables Charts are the most common visual you'll see such as line and bar charts, whereas tables can be used in documents and emails for good control of layout. ### Structure Below is an overview of all the control nodes associated with a novem plot. Most of these have sensible default values so as a user you should not need to worry about most of these endpoints. But they are there in case you want to have more control over your visualisation. You'll find separate documentation sections for most of these. ```bash plot_name => Name ├── config => Configuration options │ ├── axis => Axis options │ │ ├── location => │ │ ├── anchor => │ │ ├── ticks => │ │ ├── domains => │ │ ├── scale => │ │ ├── margin => │ │ ├── overlap => │ │ ├── labels => │ │ ├── multi => │ │ ├── padding => │ │ ├── offset => │ │ └── range => │ ├── bg => Plot Background related controls │ │ ├── type => The type of background (grid, lines) │ │ ├── align => Control background axis alignment │ │ ├── stroke => Control background stroke styles │ │ ├── color => Control background box and line colors │ │ ├── index => Which background boxes/lines to draw │ │ ├── size => Background sizing │ │ └── labels => Control background label positioning │ ├── colors => │ │ ├── type => │ │ └── colors => │ ├── table => table specific settings │ │ └── cell => Format table cells │ │ ├── align => number alignment │ │ ├── border => control table borders │ │ ├── text => font information (weight, style, color) │ │ ├── format => number and text format │ │ ├── chart => in-cell charts │ │ ├── overflow => overflow behavior │ │ ├── priority => column priority on small screens │ │ ├── padding => cell padding │ │ ├── width => cell width │ │ └── merge => merge cells across cols and rows │ ├── legend => │ │ ├── type => The kind of legend │ │ ├── position => Position of the legend │ │ ├── layout => │ │ └── format => Format of the individual legend item │ ├── labels => │ │ └── anchor => │ ├── interactive => │ │ ├── format => │ │ └── highlight => │ ├── render => │ │ ├── template => Template to render against │ │ ├── scale => │ │ └── overlap => Read-only overlap report │ ├── custom => Custom css/js for custom plots │ ├── projection => Map projection │ ├── zoom => Named geographic extent │ ├── position => Map focus, anchor, bearing, and relative scale │ ├── caption => Caption below chart │ ├── comments => Enable/disable comment threads │ ├── theme => │ ├── title => Title of chart │ └── type => bar, sbar, gbar etc... ├── complications => │ ├── bands => │ │ ├── config => │ │ │ └── label_position => │ │ └── data => │ ├── lines => │ │ ├── config => │ │ │ ├── color => │ │ │ ├── stroke => │ │ │ └── label_position => │ │ └── data => │ └── marks => │ ├── config => │ │ └── label_position => │ └── data => ├── data => data to chart ├── description => Description (meta) ├── name => Name (meta) ├── summary => One-line summary (meta) ├── vars => Attached key metrics ├── files => Pre-rendered outputs (png, pdf, csv ...) ├── assets => Uploaded images and fonts └── shared => Sharing information ├── +org~group => Shared with an org group ├── @username~group => Shared with a user group └── public => Shared with everyone ``` > **Note:** A plot's `data` is capped at **2,000,000 characters** (\~2 MB). Pre-aggregate, downsample, or drop unused > columns to stay under it. See [Size limits](/docs/reference/api/files.md#size-limits) for what happens when you exceed a > cap and the limits on other resources. ### Reference - [Config](/docs/reference/plot/config/overview.md) — every plot configuration path. - [Charts](/docs/reference/plot/charts/overview.md) — the chart types and their options. - [Tables](/docs/reference/plot/tables/overview.md) — table and e-mail table types. - [Vars](/docs/reference/plot/vars/overview.md) — plot variables for live values. ## Documents Novem documents let you create rich, multi-page documents from novem flavoured markdown. Embed live visualisations, reference live variables, add footnotes and references, all rendered into web pages, PDFs, and presentations. Impatient? Check out our [quick start guide](/docs/start/doc.md). ### Structure Documents, like most other novem visuals, follow the same hierarchical folder structure that you're used to seeing. A document has a primary file `content` where the markdown is stored, along with supporting configuration. ```text quarterly_report => Document name ├── assets => Images and fonts used by the document ├── config => Configuration options │ ├── theme => Color/logo theme │ └── type => Layout: doc, pres or blog (see below) ├── content => Document content (novem markdown) ├── description => Description (meta) ├── files => Rendered outputs (doc.pdf, doc.pptx) ├── name => Display name (meta) ├── summary => Short summary (meta) └── shared => Who can view the document ├── +org~group => Shared with an org group ├── @username~group => Shared with a user group └── public => Shared with everyone ``` This is the simplified view. The full endpoint listing (vars, tags, threads, and notifications) lives in the [document API reference](/docs/reference/doc/api.md). ### Document types The `config/type` setting picks the layout your document renders in. All three share the same markdown. Switching type changes only the page geometry and flow. - **`doc`** (default, alias `docs`) — paginated **A4 pages**. Each `{{ page }}` is a sheet that can be portrait (the default) or landscape. Best for reports and anything you want to print. - **`pres`** — **16:9 presentation slides** at PowerPoint "Widescreen" dimensions (13.333in × 7.5in). Each `{{ page }}` is a slide and defaults to landscape. Exporting to `.pptx` or PDF produces a deck that drops straight into a standard PowerPoint file. - **`blog`** (alias `web`) — one **continuous flowing column** with no page boxes; page breaks become visual dividers. Best for articles and on-screen reading. Set it like any other config value: ```bash # turn a document into a 16:9 slide deck novem -d -w config/type pres ``` ### How documents relate to mails Documents and [mails](/docs/guides/mail.md) share the same novem markdown foundation: the same section syntax (`{{ }}`), the same visualisation embedding, the same styling controls. If you're familiar with novem mails, documents will feel natural. Documents extend mails with: - **Multi-page support** — split your document into pages with `{{ page }}` - **Frontmatter** — YAML metadata at the top of your document - **Page layouts** — portrait, landscape, multi-column - **Variables** — embed live values from your visualisations inline - **Span sections** — footnotes, references and inline colour styling - **Header config** — attach layout hints to headings ### Creating a document Creating a document is similar to other novem visuals: a `HTTP PUT` request to the `https://api.novem.io/v1/vis/docs/DOC_NAME` endpoint. Documents can also be created using any of our [libraries](/docs/guides/libraries.md) or [our web page](https://novem.io/). **CLI** ```bash # create a novem document novem -d -C # add content from a file cat report.md | novem -d # share publicly novem -d -w shared/public true ``` **python** ```python from novem import Doc doc = Doc('quarterly_report') # create doc object with open('report.md', 'r') as f: doc(f) # add content doc.shared.public = True # share publicly ``` **Note:** sharing a document with `public` requires your novem profile itself to be public; private accounts can share with users and groups, but not with the world. > **Note:** A document's content is capped at **5,000,000 characters** (\~5 MB). > See [Size limits](/docs/reference/api/files.md#size-limits) for what happens when > you exceed a cap and the limits on other resources. ### Exporting Every document is rendered server-side and can be downloaded as a **PDF** or a **PowerPoint deck** from its `files/` folder; `pres` documents export as 16:9 slides that drop straight into PowerPoint: ```bash # download the PDF curl -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/vis/docs/quarterly_report/files/doc.pdf -o report.pdf # download the PowerPoint deck curl -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/vis/docs/quarterly_report/files/doc.pptx -o report.pptx ``` ### Reference - [API](/docs/reference/doc/api.md) — every endpoint of the document resource. - [Content](/docs/reference/doc/content.md) — the document content format. - [Novem Markdown](/docs/reference/markdown/overview.md) — syntax and every section type. - [Sections](/docs/reference/markdown/sections.md) — the section grammar and common parameters. ## Mails Novem allows you to automatically create e-mails using our standard api. We make it easy to embed your charts, tables, grids and documents as well as manage your recipients. Impatient? Check out our [quick start guide](/docs/start/mail.md) and [examples](/docs/reference/mail/content.md#examples). ### Structure E-mails (or mails for short), like most other novem visuals follows the same hierarchical folder structure that you're used to seeing. Mails are mostly similar to Documents in that it has a primary file `content` where most of the information for the mail is stored. In addition to the content file, there are also supporting files and folders containing information such as recipients, attachments, configuration etc. Below is an illustrative example of an overall e-mail structure. ```text daily_email_summary => E-mail Name ├── config => Configuration options │ ├── theme => Color theme for the e-mail │ ├── type => Layout type for the e-mail │ ├── reply_to => Add your registered e-mail as reply-to │ ├── size => E-mail size xs | s | m | l │ ├── subject => Subject line of the e-mail │ ├── enabled => Kill switch for production sends │ └── options => Behavioural switches │ └── comments => Allow comments on the web view ├── recipients => Who should receive the e-mail │ ├── to => List of primary recipients │ ├── cc => List of cc recipients │ └── bcc => List of bcc recipients ├── content => Content of e-mail ├── status => Trigger sends, poll progress ├── stats => Send history ├── files => Rendered outputs (txt, ansi, pdf, png, pptx) ├── assets => Uploaded images and fonts for embedding ├── vars => Attached metadata variables ├── tags => Tags on the e-mail ├── threads => Comment threads from the web view ├── log => Activity log (sends, recipient changes, errors) ├── url => Web url of the e-mail (read only) ├── shortname => Auto-generated shortname (read only) ├── description => Description (meta) ├── summary => Short summary of e-mail (meta) ├── name => Name (meta) └── shared => Who can view the e-mail in the web format ├── +org~group => Shared with an org group ├── @username~group => Shared with a user group └── public => Shared with everyone ``` ### Creating an e-mail Creating an e-mail is similar to other novem visuals, a `HTTP PUT` request to the `https://api.novem.io/v1/vis/mails/MAIL_NAME` endpoint. Obviously e-mails can be created using any of our [libraries](/docs/guides/libraries.md) or [our web page](https://novem.io/) as well. **CLI** ```bash # this assumes that the novem client is installed in $PATH cat text.md | novem \ -m \ # mail to create or update -w recipients/to user@example.com \ # recipient of the e-mail -C \ # create the e-mail if it doesn't exist -S # send the e-mail ``` **python** ```python from novem import Mail mail = Mail('name') # create mail object mail.to = "user@example.com" # add recipient with open('content.txt', 'r') as f: mail(f) # add content to mail mail.send() ``` ### Reference - [Overview](/docs/reference/mail/overview.md) — the full e-mail API surface. - [Content](/docs/reference/mail/content.md) — the mail content format. - [Recipients](/docs/reference/mail/recipients.md) — address formats and tier restrictions. - [Config](/docs/reference/mail/config.md) — subject, size, theme and the other options. - [Sending](/docs/reference/mail/sending.md) — test sends, rate limits and send status. - [Renders](/docs/reference/mail/renders.md) — txt, ansi, pdf, png and pptx outputs. - [Novem Markdown](/docs/reference/markdown/overview.md) — syntax and every section type. - [Python Mail](/docs/reference/python/mail.md) — build mails programmatically. ## Repos A **repo** is a git repository hosted on novem. You push code to it like any other git remote, and novem builds that code into a container image. That image is what a [job](/docs/guides/jobs.md) runs — repos hold the *code*, jobs orchestrate and *run* it. Impatient? Jump to the [quick start](/docs/start/repos.md). ### The `job` build type Every repo has a `config/type`. Repos default to the **`job`** type, which tells novem to build each push into a chain-runnable image and publish it to your private novem registry under a canonical reference: ```text @/:latest ``` Each pushed branch and tag becomes a label on the image, with `latest` tracking your default branch. A [job](/docs/guides/jobs.md) references this image by name when it builds a [chain](/docs/reference/jobs/chains.md). > **Note:** The build runs on every push, driven by the repo's `config/type`. > The default `job` type is set for you at creation, so a fresh repo builds on > its very first push. #### Plain git hosting with `code` If you only want to host a git repository — no image, no build — set `config/type` to `code`. Pushes are stored and browsable just like a `job` repo, but novem skips building an image entirely, so there is nothing for a chain to reference. This makes a repo suitable for plain git hosting. **CLI** ```bash novem --post /code/repos/data_fetcher/config/type code ``` **API** ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: text/plain" \ --data "code" \ https://api.novem.io/v1/code/repos/data_fetcher/config/type ``` ### How novem builds your code A `job` repo is built into a container image on every push. How that image is produced depends on whether you ship a `Dockerfile`. #### No Dockerfile — novem generates one If your repo has no `Dockerfile`, novem inspects the contents and generates one for you. Today's heuristics cover the common Python case: - **A single `.py` file in the repo root** → a minimal image that runs it. - **A single `.py` file plus a `requirements.txt`** → the same, with your dependencies `pip`-installed first. The generated Dockerfile is roughly equivalent to: ```dockerfile FROM python:3-slim COPY . /app WORKDIR /app # only when requirements.txt is present: RUN pip3 install -r requirements.txt ENTRYPOINT ["python3", "/app/main.py"] ``` The repo's `log` records which heuristic ran (e.g. `python_single_file_with_requirements`), so you can confirm what novem detected. #### Bring your own Dockerfile For anything beyond the simple cases — another language, system packages, a build step — add a `Dockerfile` to the repo root and novem uses it as-is. There is no required base image or layout. > **Note:** your program receives no arguments. novem runs your image with > whatever `ENTRYPOINT`/`CMD` it defines and appends nothing (see > [how a job runs](/docs/guides/jobs.md#how-your-code-runs)), so hardcode the > paths `/input` and `/output`; they never change. An entrypoint that requires > positional arguments, such as `run.sh `, exits before doing > any work. Give them defaults instead: `"${1:-/input}"`. ### Structure Like other novem resources, a repo is a hierarchical folder structure. Below is an illustrative example. ```text data_fetcher => Repo name ├── config => Configuration options │ ├── type => Build type (job) │ └── branch │ └── default => Default branch (main) ├── url => Git clone URL ├── files => Browse the repo's files ├── branches => Branches ├── commits => Commit history ├── log => Build log ├── description => Description (meta) ├── name => Name (meta) ├── shortname => Auto-generated short id └── shared => Who can view / use the repo ├── +org~group => Shared with an org group ├── @username~group => Shared with a user group └── public => Shared with everyone ``` ### Creating a repo A repo is created with an `HTTP PUT` to the `https://api.novem.io/v1/code/repos/REPO_NAME` endpoint — from [our web app](https://novem.io/), or from the command line. The CLI has no dedicated repo flag, but its raw HTTP interface (`--get`, `--post`, `--put`) can target any API path directly — the path is everything after `/v1/`. **CLI** ```bash novem --put /code/repos/data_fetcher ``` **API** ```bash curl -X PUT -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/repos/data_fetcher ``` New repos come up as the `job` type, so there is usually nothing more to configure before your first push. ### Setting the default branch A repo's default branch is what `latest` tracks and what novem checks out when nothing more specific is requested. New repos default to `main`. Point it at another branch with an `HTTP POST` to `config/branch/default`, sending the branch name as the body: **CLI** ```bash novem --post /code/repos/data_fetcher/config/branch/default develop ``` **API** ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: text/plain" \ --data "develop" \ https://api.novem.io/v1/code/repos/data_fetcher/config/branch/default ``` The branch must already exist on the repo (push it first). To return the default to `main`, `HTTP DELETE` the same path: **CLI** ```bash novem --delete /code/repos/data_fetcher/config/branch/default ``` **API** ```bash curl -X DELETE -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/repos/data_fetcher/config/branch/default ``` ### Registering an SSH key novem speaks plain git over both SSH and HTTPS. To push over SSH, register a public key on your profile once. Keys live under your profile, not the repo, so a single key works for all of your repos. `PUT` `/v1/admin/keys/laptop` `POST` `/v1/admin/keys/laptop/key` ```text ssh-ed25519 AAAAC3NzaC1lZDI1... you@laptop ``` ### Pushing code Clone, commit and push with the git tooling you already use. Your repo's clone URL is published at its `url` path (`/code/repos//url`). **https** ```bash # authenticate as the git user with your novem token git clone https://x-token:$NOVEM_TOKEN@novem.io/u//r/data_fetcher.git cd data_fetcher # add your code, then push — this triggers a build git add main.py git commit -m "initial commit" git push origin main ``` **ssh** ```bash # register a public key first (see "Registering an SSH key" above), # then clone using the SSH URL published at the repo's `url` path git clone data_fetcher cd data_fetcher git add main.py git commit -m "initial commit" git push origin main ``` Each push builds the commit into `@/data_fetcher:latest`. Follow the build on the repo's `log`, and once it completes the image is ready to be referenced from a job. ### Next steps - [Repo quick start](/docs/start/repos.md) — create, push and build your first repo end to end. - [Jobs](/docs/guides/jobs.md) — run your built image, alone or as a chain. ## Jobs A **job** is novem's unit of code execution. Where a [repo](/docs/guides/repos.md) holds your code and builds it into an image, a job decides *how and when* that image runs: by itself, or as a multi-step pipeline that pipes one step's output into the next. Impatient? Jump to the [quick start](/docs/start/jobs.md). ### The `chains` runtime Jobs default to the **`chains`** runtime. A chains job points at one or more repo images through its `config/chains` definition and runs them via novem's worker fleet. A chain can be a single image or a directed pipeline of several. See [chains](/docs/reference/jobs/chains.md) for the full grammar. A step can also run another job rather than a repo image, which lets you build a workflow from reusable pieces: ```text fetch <- data_fetcher rollup <- /j/nightly_rollup fetch -> rollup ``` novem expands `nightly_rollup` into its own steps and runs them inline, and records a nested run for it under this job's run. See [chains](/docs/reference/jobs/chains.md#referencing-another-job) and [runs](/docs/reference/jobs/runs.md#composed-jobs-the-run-tree). ### Structure Like other novem resources, a job is a hierarchical folder structure. ```text daily_report => Job name ├── config => Configuration options │ ├── type => Runtime type (chains) │ ├── chains => Pipeline definition (repo images + DAG) │ ├── env => Environment variables (encrypted, lower-case) │ │ └── api_token => A single variable │ ├── schedule => Cron schedule (optional) │ └── enabled => Kill switch (true / false) ├── data => POST here to trigger a run ├── runs => Run history │ └── │ ├── log => The run's log │ ├── output => The run's result file(s) │ ├── stats => Run metadata (timings, trigger, …) │ └── status => processing / success / failed ├── log => Latest run's log (shortcut) ├── stats │ └── runs => Run history stats (time, trigger, duration, status) ├── description => Description (meta) ├── name => Name (meta) ├── summary => One-line summary (meta) ├── shortname => Auto-generated short id ├── tags => Tags on the job ├── vars => Job variables └── shared => Who can view the job ├── +org~group => Shared with an org group ├── @username~group => Shared with a user group └── public => Shared with everyone ``` The full key-by-key breakdown lives in the [config reference](/docs/reference/jobs/config.md). ### Creating a job A job is created with an `HTTP PUT` to the `https://api.novem.io/v1/code/jobs/JOB_NAME` endpoint, then pointed at the repo image(s) it should run via `config/chains`. **CLI** ```bash # create the job (defaults to the chains runtime) novem -j daily_report -C # point it at a repo image; the chain is a single step here echo "report_builder" | novem -j daily_report -w config/chains ``` **API** ```bash # create the job curl -X PUT -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/jobs/daily_report # define the chain curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: text/plain" \ --data "report_builder" \ https://api.novem.io/v1/code/jobs/daily_report/config/chains ``` `report_builder` is the name of one of your repos; novem resolves it to your built image `@/report_builder:latest`. See [chains](/docs/reference/jobs/chains.md) for multi-step pipelines. ### Environment variables Jobs inject environment variables into the running container. Set them under `config/env`. Values are encrypted at rest and never returned in plaintext. Create each variable with a **lower-case** name. **CLI** ```bash # value can be piped on stdin to keep it out of your shell history printf '%s' "$SOME_SECRET" | novem -j daily_report -w config/env/api_token # list the variables that are set (names only) novem -j daily_report --tree config/env ``` **API** ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: text/plain" \ --data-binary @- \ https://api.novem.io/v1/code/jobs/daily_report/config/env/api_token <<< "$SOME_SECRET" ``` Inside the running job both the lower-case name and an upper-cased variant are injected, so `api_token` and `API_TOKEN` are both visible to your code. > **Note:** Environment variables are encrypted for the job's runtime when you > set them. If you change a job's `config/type` after setting variables, the > next run is rejected (`409`) with a message naming the affected variables; > re-set them so they are re-encrypted for the new runtime. ### How your code runs When a job runs, novem mounts the run's input at **`/input`** and collects results from **`/output`**, then starts your repo's image. The image runs with whatever `ENTRYPOINT`/`CMD` it defines and receives no extra arguments, so your code reads from `/input` and writes to `/output` directly. Those mount points never change. ```python import os input_dir = "/input" output_dir = "/output" for name in os.listdir(input_dir): ... # process each input file # write your results into output_dir ``` This is a **convention, not an enforced contract**: novem does not inspect or wrap your code, it only mounts the two directories. The convention is just: read `/input`, write `/output`. #### What's in `/input` A run's input depends on how it was triggered: - **A JSON payload** (`novem -j -R`, or `POST .../data`) arrives as `/input/input.json`. - **Uploaded files** (`-R @data.csv`, or a browser attachment) arrive as `/input/`, original names preserved. Both can be present at once, so when you expect an upload, prefer real files over `input.json` rather than assuming a single input. #### What to write to `/output` Write one or more result files into `/output`. novem packages whatever it finds: - **One file** → returned as-is, with its filename and a content type guessed from the extension. - **Multiple files (or subdirectories)** → bundled into a single `.zip`. - **Nothing** → the run produces no result. Any filename works: `result.txt`, `report.pdf`, `out.csv`. (The 100 MB result cap below still applies.) ### Running a job Trigger a run with the CLI's `-R` flag, or by posting to the job's `data` endpoint directly. To send input files, append each filename to the command with an `@` prefix (e.g. `@data.csv`); the `@` is what tells novem to upload the file rather than treat the word as a literal argument. Anything you send becomes the job's input, available to the first step of the chain. Runs are recorded under `runs`, each with its own `log`, `output`, `stats` and `status`. See the [runs reference](/docs/reference/jobs/runs.md) for the full picture. To pull the files a run produces back to your machine, point `-o`/ `--output` at a directory. **CLI** ```bash # trigger a run novem -j daily_report -R # to pass input files, append one or more filenames, each prefixed with @ novem -j daily_report -R @data.csv @config.json # save the run's output files to disk with -o/--output novem -j daily_report -R -o ./out # read the latest run's log novem -j daily_report -r log ``` **API** ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: application/json" \ --data '{}' \ https://api.novem.io/v1/code/jobs/daily_report/data ``` > **Note:** A run's result is capped at **100 MB**. Mail clients and mail servers > enforce strict size limits, so the cap keeps a job's result deliverable to the > inboxes and downstream consumers it tends to feed. A larger result fails in > finalisation and nothing is stored; the run `log` shows a `413` error like > `Job result is too large: 142.0 MB exceeds the 100 MB limit.`. Trim or compress > the output, or split the work across chain steps, to stay under the cap. Jobs can also run automatically on a cron [`config/schedule`](/docs/reference/jobs/schedule.md), and be paused with the `config/enabled` kill switch. ### Next steps - [Job quick start](/docs/start/jobs.md) — run a repo image end to end. - [Chains](/docs/reference/jobs/chains.md) — pipe steps together into a pipeline. - [Runs](/docs/reference/jobs/runs.md) — logs, output, stats and retention. - [Config](/docs/reference/jobs/config.md) — every configuration key and metadata endpoint. ## Spaces > **Note:** Spaces are an **early-access** feature. The endpoints below are > gated behind a per-account feature flag and return `404` unless it's enabled > on your account. Reach out to us if you'd like to try them. Most novem resources are built around one kind of content: a [plot](/docs/guides/plot.md) holds data, a [repo](/docs/guides/repos.md) holds git history. A **space** holds *files*: any type, any folder structure, up to your plan's quota. Think of it as a cloud folder that lives next to your [repos](/docs/guides/repos.md) and [jobs](/docs/guides/jobs.md) under the code surface. Spaces are designed to be the storage layer for the rest of the platform. A [job](/docs/reference/jobs/spaces.md) can already mount one as a folder; sync clients are on the roadmap. Everything else is a clean HTTP file API: upload, download, list, move, delete, share. ### Structure Like every novem resource, a space is a small folder hierarchy: ```text project_data => Space name ├── content => Your files and folders live here │ └── ... => Arbitrary nesting, up to 64 levels deep ├── changes => Change journal (for sync clients) ├── config │ └── access │ ├── ssh => Reserved for upcoming access protocols │ └── web => Reserved for upcoming access protocols ├── description => Description (meta) ├── name => Display name (meta) ├── summary => Short summary (meta) ├── shortname => Auto-generated short id ├── url => The space's URL ├── shared => Who can see / edit the space └── tags => Tags ``` Everything outside `content/` works exactly like the equivalent files on repos and jobs. The interesting part is `content/`, a real file tree with its own set of verbs, documented in full in the [spaces API reference](/docs/reference/spaces/api.md). ### Creating a space A space is created with an `HTTP PUT` to `https://api.novem.io/v1/code/spaces/SPACE_NAME`: **CLI** ```bash novem --put /code/spaces/project_data ``` **API** ```bash curl -X PUT -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/spaces/project_data ``` `DELETE` on the same path removes the space, and `PATCH` renames it: the standard novem resource verbs. ### Working with files Files live under `content/`. The verbs follow the novem convention: `PUT` creates a container (a folder), `POST` writes content (a file's bytes): **CLI** ```bash # make a folder novem --put /code/spaces/project_data/content/reports # upload a (text) file; parent folders are created automatically novem --post /code/spaces/project_data/content/reports/q1.csv "$(cat q1.csv)" # read it back novem --get /code/spaces/project_data/content/reports/q1.csv ``` **API** ```bash # make a folder curl -X PUT -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/spaces/project_data/content/reports # upload a file (parent folders are created automatically) curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: text/csv" --data-binary @q1.csv \ https://api.novem.io/v1/code/spaces/project_data/content/reports/q1.csv # read it back curl -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/code/spaces/project_data/content/reports/q1.csv ``` A `GET` on a folder (or on `content` itself) returns a JSON listing in the same shape as every other novem directory. `PATCH` renames or moves a file or a whole folder tree; moves are instant regardless of size, since blobs are stored by id, not by path. `DELETE` removes a file; deleting a non-empty folder requires an explicit `?recursive=true`. Every file carries an **ETag** that changes on every write, and the write verbs honor `If-Match` / `If-None-Match` preconditions, so concurrent editors and scripts can detect conflicts instead of silently overwriting each other. The full verb-by-verb behavior, including status codes and conditional headers, is in the [spaces API reference](/docs/reference/spaces/api.md). #### File names The store is **case-preserving but case-insensitive**: `Report.csv` keeps its casing, but you cannot create `report.csv` next to it. Names may use spaces, unicode and mixed case; a small set of characters that cannot exist on every platform (`< > : " / \ | ? *`, control characters, Windows reserved names like `CON`, trailing dots or spaces) is rejected at write time. Paths can nest up to 64 levels and 1024 bytes. ### Tracking changes Every mutation in a space (create, update, move, delete) is recorded in an append-only journal with a per-space sequence number. `GET .../changes?since=N` returns everything that happened after cursor `N`, including delete tombstones and both sides of moves. This is what future sync clients will be built on, and it's available to your own tooling today: ```bash novem --get "/code/spaces/project_data/changes?since=0" ``` ### Sharing A space is shared like any other novem resource, by creating entries under `shared/`. The grantee's group role determines what they can do: - **read** access — list and download everything under `content/`, and read the `changes` journal. - **write** access — full control of `content/`: create, modify, move and delete. Files written by a collaborator count against the *owner's* quota. Spaces cannot be made public yet; content is only ever visible to the owner and explicit shares. ### Quotas Space storage is tied to your subscription plan: | Limit | Free | Basic | Premium | Enterprise | | -------------------- | ------ | ------ | ------- | ---------- | | Spaces per user | 1 | 5 | 20 | 20 | | Max single file size | 100 MB | 100 MB | 100 MB | 100 MB | | Max space size | 5 GB | 5 GB | 5 GB | 5 GB | Writes that would exceed a limit are rejected with `413`. A space can hold at most 1,000,000 files and folders. ### Using a space from a job A [job](/docs/guides/jobs.md) can mount a space and work with it as ordinary files, instead of calling the API. Map a name to the space in the job's `config/spaces` and it appears as a folder inside the run: ```text data_in => /u/alice/s/project_a,ro scratch => /u/alice/s/scratch,rw ``` Mounts default to read-only, and changes under a `rw` mount reach the space as the job writes them, not in one batch when the run ends — a run that fails keeps whatever it had already written. See [job spaces](/docs/reference/jobs/spaces.md) for the file format, access rules and limits. ### Next steps - [Spaces API reference](/docs/reference/spaces/api.md) — every endpoint, verb and status code on the spaces surface. - [Job spaces](/docs/reference/jobs/spaces.md) — attaching a space to a job. - [Jobs](/docs/guides/jobs.md) and [Repos](/docs/guides/repos.md) — the neighbours under the code surface. ## API ### Structure The novem API resembles a regular hierarchical file system: `folders` that contain `files` or other folders. You interact with it over HTTP much like you would a local filesystem: `GET` to read a file or list a folder, `PUT` to create one, `POST` to write a file's content, and `DELETE` to remove it. Each endpoint also carries unix-style read, write and delete permissions, surfaced in folder listings and via the `OPTIONS` verb. The API is located at `https://api.novem.io/v1/`. For the rest of this document we use the shorthand `/` to mean the base of the API, so that e.g. `/whoami` corresponds to `https://api.novem.io/v1/whoami`. Most paths expect plain text (`text/plain`). Resources live under a handful of top-level segments: `/vis` for your plots, mails, docs and grids, `/code` for jobs and repos, and `/admin` for your profile, groups and settings. When you create a folder its underlying structure is already populated: creating a plot under `/vis/plots`, for example, automatically creates the folder beneath it with files such as `data`, `url` and `shortname` and folders such as `config` and `shared`. ### Interacting with the API A `GET` on a folder returns its contents as a list of JSON objects, where `name` is the resource, `uri` its path, `permissions` the "file" permissions and `actions` the valid HTTP verbs. A `GET` on a file returns the file's content directly. `PUT` creates folders: `PUT /v1/vis/plots/en_letter_freq` creates a new plot named `en_letter_freq`. These names may only contain lowercase alphanumeric characters and underscores. `PUT` is only ever used to create new files and folders, never to update ones that already exist. `POST` writes or updates the content of a file. To turn the plot above into a bar chart you `POST` `bar` to `/vis/plots/en_letter_freq/config/type`. When you no longer need the plot, `DELETE /v1/vis/plots/en_letter_freq` removes it. ### Viewing the output All novem visualisation rendering happens on our servers, and the output is made available in many formats. The easiest is to view it online by following the url found under the `url` endpoint in the root of every visualisation folder. We also provide pre-rendered output under each visualisation's `files/` folder, where `png`, `pdf`, `txt` and `ansi` versions are available. Every visual also carries a globally-unique [shortname](/docs/reference/api/terminology.md#shortname) for addressing it without its owner prefix; this is what the public share links and render aliases are keyed on. ### Sharing Novem visuals are private to their owner by default, but can be shared with other users, groups and the world. The `shared/` folder holds one entry per target you share with: - `@jones` — shared directly with the user `jones`. - `@smith~plots` — shared with the `plots` group created by the user `smith`. - `+company~plots` — shared with the `plots` group created by the organisation `company`. - `public` — shared with the world. To remove access, `DELETE` the corresponding entry in `shared/`. The [sharing reference](/docs/reference/api/sharing.md) covers the full mechanism, including how to grant write access and how to read content shared with you. ### Reference - [Terminology](/docs/reference/api/terminology.md) — the FQNP, shortnames and public read aliases. [Tags](/docs/reference/api/tags.md), [Social](/docs/reference/api/social.md), [Slicing](/docs/reference/api/slicing.md), [Vars](/docs/reference/api/vars.md) and [Files](/docs/reference/api/files.md) round out the API reference. - [Novem Markdown](/docs/reference/markdown/overview.md) — the content format. ## Themes ### Why themes Often you want to create several different assets, styles and documents that all look similar. It's possible to do this hardcoded in code, but it's easier if this information can live in a separate structure and be re-used. To facilitate this novem offers themes which let you define your visual identity once and reuse it everywhere. Set `--novem-accent` to your corporate blue and every callout border, link, footnote ref and chart accent follows along. No per-vis configuration is needed and the change travels into embedded plots automatically. A custom JavaScript plot embedded in a doc can read the parent's variables both through CSS using `var(--novem-text)` and through a JavaScript object as `render.theme.text`. The plot behaves like a native part of the document. Dark mode flips automatically as well. The engine keeps a parallel set of dark-mode defaults for every style variable, so a theme that doesn't explicitly handle dark mode still looks correct in both modes. ### Theme structure The theme leverages the Cascade in Cascading Style Sheets (CSS) by predefining a set of variables that are then user overridable. By overriding the existing css variables in your plot you can have a unique style when the chart is viewed alone, but inherit corporate styles when embedded into doc or an e-mail. Novem styles compose top-down through three layers, where each layer can override the previous one. ``` 1. Baseline styles structural layout + sane --novem-* defaults (built into the engine, not user-editable) 2. Theme brand: novem (default) | custom | +org/theme overrides --novem-* values + selector refinements 3. Per-vis CSS author overrides for one specific vis same primitives, scoped to this document ``` Baseline styles is built into the engine and not user-editable. It sets the default values for every primitive. The theme sits on top, swapping in its own `--novem-*` values and adding selector refinements. Per-vis CSS layers further on top to fine-tune a single visualisation. You almost never need `!important` because specificity follows the cascade naturally. ### Choosing a theme Every doc, grid, plot or mail has a `/config/theme` value. The default is `novem` which gives you the built-in theme. You can also use `custom` to opt out of the built-in theme entirely and let your `custom.css` be the whole theme surface, or `+org/yourorg` to use an org theme that bundles CSS, fonts and assets authored once by your org admin. You can set the theme via the CLI, the API or the editor. ```bash # CLI novem -p my-plot -w config/theme custom # API curl -X POST -d "custom" \ https://api.novem.io/v1/vis/plots/my-plot/config/theme ``` When `theme = custom`, the only style sources are the engine skeleton plus your `custom.css`. When `theme = novem`, the built-in theme sits takes over and will override your custom.css values. This is how a chart, when embedded in a plot, will match the parent theme. ### Patterns and pitfalls A handful of recurring patterns emerge once you've themed a few visualisations. Always use the variables, not hardcoded values. A chart that hardcodes `stroke: "#212529"` looks invisible in dark mode. Reach for `var(--novem-axis-color)` in CSS, or `render.theme.axisColor` in JS, and the engine flips them for you. Test in dark mode. The webapp toggles dark mode with the moon icon in the top-right. A theme that only handles light mode is half a theme. Don't abuse `!important`. The cascade gives you all the layering you need: skeleton, then theme, then per-vis. If you find yourself reaching for `!important`, you're usually fighting the cascade in the wrong direction. It's idiomatic to keep your brand colours as private variables like `--brand-navy` and re-route `--novem-*` to point at them. That way the brand has a single source of truth and the primitives keep their semantic meaning. Variables propagate into iframes, so anything you set on `.novem--doc--page` shows up in embedded plots automatically. You don't need to repeat your theme inside each plot. ### Reference - [Colors](/docs/reference/theme/colors.md) — named colors, scales and theme variables. - [Typography](/docs/reference/theme/typography.md) — font families, sizes and weights. - [Document](/docs/reference/theme/document.md) — document-only theme primitives. - [Charts](/docs/reference/theme/charts.md) — chart theme variables. - [Org themes](/docs/reference/theme/org-themes.md) — reusable organisation themes. ## Example The best way to understand novem theming is to see a complete example. Below is a small `custom.css` that rebrands a doc and every plot embedded inside it. To follow along, set `/config/theme` to `custom` first, then push this CSS to your visualisation. ```css .novem--doc--page { /* Brand tokens (private to this CSS) */ --brand-navy: #0c2340; --brand-gold: #c5a55a; --brand-light: #f4f1eb; /* Re-route engine primitives to brand tokens */ --novem-font-body: "Inter", "Segoe UI", system-ui, sans-serif; --novem-font-heading: "Inter", "Segoe UI", system-ui, sans-serif; --novem-accent: var(--brand-navy); --novem-link: var(--brand-navy); --novem-surface: var(--brand-light); --novem-table-header-bg: var(--brand-navy); /* Categorical chart palette to brand sequence */ --novem-color-1: var(--brand-navy); --novem-color-2: var(--brand-gold); --novem-color-3: #6e8aab; --novem-color-4: #a5895a; --novem-color-5: #2c4a6e; } /* Selector refinement on top of the variable layer */ .novem--doc--heading--2 { border-bottom: 2px solid var(--brand-gold); padding-bottom: 6px; } /* Dark-mode overrides — same primitives, different values */ &[data-dark-mode] .novem--doc--page { --brand-navy: #8ba4d4; --brand-gold: #d4b86a; --brand-light: #1a1a2e; } ``` The first block defines a few private brand tokens and re-routes the engine primitives to point at them. Anything embedded in this doc — plots, grids, tables and callouts — picks up the navy and gold palette without further configuration. Tables use navy headers because we've re-routed `--novem-table-header-bg` directly. The second block adds a selector refinement to give h2 headings a gold underline. This is layered on top of the variable substitution and illustrates how a theme can go beyond simple primitive overrides when the visual treatment calls for it. The third block flips the brand tokens for dark mode. Because the `--novem-*` primitives still point at the brand tokens, the change cascades through to every consumer automatically. ## Libraries This section introduces Novem's libraries, which bring the power of the Novem API to various environments. Using these libraries allows you to integrate with the Novem API directly from wherever you get your work done. Check out the following library guides for step-by-step instructions. ### VS Code The [novem VS Code extension](https://marketplace.visualstudio.com/items?itemName=novem.novem-vscode) lets you browse, edit and preview your novem visuals — plots, documents and e-mails — without leaving the editor. Install it from the marketplace by searching for `novem`, or from the Quick Open prompt: ```text ext install novem.novem-vscode ``` The extension will prompt you to sign in on first use — no other setup required. ### Reference - [Python Plot](/docs/reference/python/plot.md), [Python Mail](/docs/reference/python/mail.md) and [Utilities](/docs/reference/python/utilities.md) — the python API reference. - [Novem Markdown](/docs/reference/markdown/overview.md) — the content format used by documents and mails across all libraries. ## CLI > **Note:** this is a highly technical subject, familiarity with the command line and coding is advised. If you want to > try something easier check out our app or excel interface. ### Installation The novem command line interface, or cli, is distributed using the python [Python Package Index](https://pypi.org/project/novem/), and you can install it using your preferred python command installation procedure. The source code is also available on [GitHub](https://github.com/novem-code/novem-python) if you want to manually download or install the cli. To avoid contaminating the global namespace we recommend installing novem in a separate environment, ideally using a tool such as [pipx](https://pypa.github.io/pipx/). For the rest of these examples we'll use pipx for installation. #### Getting started To install the novem cli please run the following in a terminal window with pipx available: `pipx install novem`. ```bash # example of install workflow username@computer:~/$ pipx install novem username@computer:~/$ _ ``` Once novem is installed you need to generate and store an API key to get access. This can be done with the following commands: `novem --init` ```bash username@computer:~/$ novem --init • novem.io username: • novem.io password: ✓ authenticated ✓ token created ✓ new token "novem-cli-computer-rj8chk2j" saved to ~/.config/novem/novem.conf username@computer:~/$ _ ``` Congratulations, your novem cli is now installed, configured and authenticated. #### Configuration and options The novem cli stores a configuration file in `novem/novem.conf` under the default config folder location. On Windows it uses `LOCALAPPDATA` or `APPDATA`, whilst on Linux and Mac it tries to use `XDG_CONFIG_HOME` but defaults to `~/.config/` if not present. The novem config files are very similar to windows ini style files as supported by python's [config parser](https://docs.python.org/3/library/configparser.html#supported-ini-file-structure). By default you should not need to make any manual changes to the novem config file, but it can be helpful to know how it works. In short a configuration file consists of sections, each led by a \[section] header, followed by key/value entries separated by an equals sign (=). Section names are case sensitive but keys are not. Leading and trailing whitespace is removed from keys and values. The novem config file consists of three types of sections that is shared among all novem applications, including fuse, the [python library](/docs/guides/python.md) or this cli. These sections are general, application and profile specific. ##### General The `[general]` section allows you to set options that is true for all applications using the file. Here you can control things such as the API url, logging, default profile etc. The currently supported global options are: - `profile` - the default profile to use - `api_root` - the API url to use for the cli ```ini [general] profile = novem_demo api_root = https://api.novem.io/v1/ ``` ##### Application (app) The application specific sections start with the string `app` followed by a colon `:`, after the colon follows the name of the novem application. Currently only `fuse`, `cli` and `pylib` are supported. The different app sections allows you to override the `[general]` values as well as supply app specific options. ```ini [app:cli] profile = novem_research # change user for the cli ``` ##### Profile The final section type is the profile sections. Profile sections start with the string `profile:` followed by a profile name. This is the value referred to in the `[general]` and `[app]` sections above. As an example, the profile section for two users, novem\_research and novem\_demo could look as follows. ```ini [profile:novem_demo] username = novem_demo token_name = novem-python-computer-as123ajs token = [profile:novem_research] username = novem_research token_name = novem-python-computer-vgjaa1tb token = ``` Whilst the username and profile name can be the same, this is not mandatory. ### Appendix For the full list of commands and flags available in the cli version you have installed, run `novem -h` (or `novem --help`). ### Reference - [Plot config](/docs/reference/plot/config/overview.md) — every path you can `-w` to. - [Novem Markdown](/docs/reference/markdown/overview.md) — the content format for documents and mails. - [Terminology](/docs/reference/api/terminology.md) — FQNPs and the API vocabulary. ## Plot For this document we assume that you've already installed the novem cli and that the `novem` command is available in your `$PATH`. If you need help installing the novem cli please see the [installation instructions](/docs/guides/cli.md#installation). Plots are selected with the `-p` flag. On its own, `-p` lists all of your plots; with a name it selects that plot for the operations described below. ```bash # list all of your plots novem -p # select a single plot to operate on novem -p my_plot ``` ### Creating a plot Add `-C` to create the plot if it doesn't already exist. The `--type` flag is a shorthand for writing `config/type`, and sets the chart type. ```bash # create a new plot and set it to a bar chart novem -p my_plot -C --type bar ``` ### Adding data The data lives at the plot's `data` path. Write to any path with `-w` — the value can be supplied inline, read from a file with `@filename`, or piped on stdin. ```bash # from a file novem -p my_plot -w data @data.csv # or piped on stdin cat data.csv | novem -p my_plot -w data ``` ### Setting title, caption and other config Everything under `config/` is set the same way, with `-w` and the relative path. ```bash novem -p my_plot -w config/title "Quarterly revenue" novem -p my_plot -w config/caption "Source: internal finance data" ``` For a subtitle, write two newline-separated lines. The [title reference](/docs/reference/plot/config/title.md) covers per-line styling and alignment: ```bash novem -p my_plot -w config/title $'Quarterly revenue tx:18\nUnaudited tx:12' ``` To edit a longer value interactively, `-e` opens the path in `$EDITOR` and saves on exit: ```bash novem -p my_plot -e config/caption ``` ### Viewing a plot Read any path back with `-r`. The `url` path returns the shareable link, and `-x` is a shorthand for a terminal-friendly render (identical to `-r files/plot.ansi`). ```bash # print the shareable url novem -p my_plot -r url # preview the plot directly in your terminal novem -p my_plot -x ``` ### Sharing Shares are managed with `-s`. A bare `-s` lists the current shares; combine it with `-C` to add a share and `-D` to remove one. ```bash # share publicly novem -p my_plot -C -s public # list current shares novem -p my_plot -s # stop sharing publicly novem -p my_plot -D -s public ``` ### Tags Select a tag with `-t` (for example `fav`, `pin`, `hide`, `wip`, `archived`, a `+usertag` or a `=categorytag`); a bare `-t` lists the plot's current tags. ```bash # mark a plot as a favourite novem -p my_plot -t fav # feature this plot on your profile feed (up to four newest pins) novem -p my_plot -t pin # keep this plot out of lists, search, discovery and feeds novem -p my_plot -t hide # list the plot's tags novem -p my_plot -t ``` ### Deleting a plot ```bash novem -p my_plot -D ``` ### Reference - [Config](/docs/reference/plot/config/overview.md) — every plot configuration path. - [Charts](/docs/reference/plot/charts/overview.md) — chart types and their options. ## Mail For this document we assume that you've already installed the novem cli and that the `novem` command is available in your `$PATH`. If you need help installing the novem cli please see the [installation instructions](/docs/guides/cli.md#installation). E-mails are selected with the `-m` flag. On its own, `-m` lists all of your e-mails; with a name it selects that e-mail for the operations described below. ```bash # list all of your e-mails novem -m # select a single e-mail to operate on novem -m monthly_update ``` ### Creating an e-mail Add `-C` to create the e-mail if it doesn't already exist. ```bash novem -m monthly_update -C ``` ### Adding recipients Recipients live under the e-mail's `recipients` folder. The `--to`, `--cc` and `--bcc` flags are shorthands for writing them. A recipient can be a novem user, a novem group (`@user~group` / `+org~group`), or a plain e-mail address; separate several with a `;`. ```bash novem -m monthly_update --to "novem_demo; investors@example.com" novem -m monthly_update --cc "@acme~analysts" ``` See [recipients](/docs/reference/mail/recipients.md) for the address rules and any sending restrictions on your account. ### Setting the subject ```bash novem -m monthly_update --subject "Monthly update — March" ``` ### Writing the content The body is [novem markdown](/docs/reference/mail/content.md) written to the e-mail's `content`. Pipe a file in on stdin, or write it with `-w`. ```bash # pipe a markdown file as the body cat body.md | novem -m monthly_update -w content # create, set subject + recipient, and pipe the body in one go cat body.md | novem -m monthly_update -C \ --subject "Monthly update" --to investors@example.com -w content ``` ### Sending Send a **test** copy to your own registered address with `-T`, and send to the real recipients with `-S`. Both trigger a fresh server-side render first, so the e-mail always reflects your latest changes. ```bash # send a test to yourself novem -m monthly_update -T # send to the recipients novem -m monthly_update -S ``` **Note:** free accounts can only send to their own registered, verified address. ### Viewing Read any path back with `-r`; the `url` path returns the shareable link. ```bash novem -m monthly_update -r url ``` ### Sharing and tags Like other novem resources, e-mails can be shared and tagged. Manage shares with `-s` (add with `-C`, remove with `-D`, a bare `-s` lists them) and tags with `-t`. See the [plot](/docs/guides/cli/plot.md#sharing) page for the shared-flag patterns — they work identically for e-mails. ### Deleting an e-mail ```bash novem -m monthly_update -D ``` ## Python The novem python API provides utility and convenience functions for using the novem API from python. The primary use-case is creating charts based on pandas dataframes, but other relevant novem functionality is also exposed through the api. This document explains the overall design of the library and how to use it, for examples of specific use-cases please see the relevant sections on plots and e-mails. #### Installation The novem python library requires python 3.8.5 or higher and is available from PyPI. The source code can also be found on our [GitHub](https://github.com/novem-code/novem-python). ```bash pip install novem ``` Once the novem library is installed you can run `python -m novem --init` (or just `novem --init` depending on your system setup) to generate a new token and create the default config file. For more details, please see documentation for the [novem cli](/docs/guides/cli.md). ```bash username@computer:~/$ python -m novem --init • novem.io username: • novem.io password: ✓ authenticated ✓ token created ✓ new token "novem-python-computer-rj8chk2j" saved to ~/.config/novem/novem.conf username@computer:~/$ _ ``` ### Configuration and authentication Every novem object needs a token and an API root to reach the platform. They are resolved, in order of precedence, from: 1. an explicit keyword argument on the object (or a [`Session`](#multiple-accounts), see below) 2. values set programmatically on `novem.config` 3. the `NOVEM_TOKEN` / `NOVEM_API_ROOT` environment variables 4. the config file written by `python -m novem --init` The config file (step 4) is the simplest setup. To configure novem programmatically instead — handy in notebooks, scripts or CI — set a token on the global `novem.config` once, and every object created afterwards picks it up automatically: ```python import novem novem.config.set_token("your-token") plot = novem.Plot("my-plot") ``` `novem.config` also exposes `set_api_root(...)` to point at a non-default API, and `use_profile(...)` to select a profile from the config file. To override the global default for a single object, pass the token straight to the constructor — an explicit argument always wins: ```python plot = novem.Plot("my-plot", token="your-token") ``` #### Multiple accounts A `Session` captures connection settings (a token and API root, or a config-file profile) and constructs objects bound to them without touching the global defaults — useful when working against several accounts at once: ```python import novem work = novem.Session(profile="work") personal = novem.Session(profile="personal") # copy a plot's data from one account to the other personal.Plot("earnings").data = work.Plot("earnings").data ``` ### Plot The `plot` module exposes a novem plot class that maps to the `/v1/vis/plots/` API end-point. The class takes one positional parameter, the plot name, and several optional named parameters. All the named parameters in the constructor are also available as properties. ```python """ Below we show the various ways you can set the options on your novem plot """ from novem import Plot # everything in the constructor barchart = Plot('barchart', type='bar', title='barchart title', caption='caption', ) # property approach barchart = Plot('plot_name') barchart.type = 'bar' barchart.title = 'barchart title' barchart.caption = 'caption' ``` ### Mail The `mail` module exposes a novem plot class that maps to the `/v1/vis/mails/` API end-point. The class allows two primary way of constructing an e-mail. Either by supplying a novem markdown document as a content file or constructing the e-mail programmatically. ##### The markdown approach ```python from novem import Mail mail = Mail('name') # create mail object mail.to = "user@example.com" # add recipient with open('content.txt', 'r') as f: mail(f) # add content to mail mail.send() ``` ##### The pythonic approach ```python from novem import Mail from novem.mail import PreviewSection, MarkdownSection mail = Mail('name') # create mail object mail.to = "user@example.com" # add recipient mail.subject = "Test e-mail" mail.add_section(PreviewSection(""" Preview summary that is not visible in the main body of the mail """)) mail.add_section(MarkdownSection(""" Markdown text for text section """)) mail.send() ``` ### Reference The full python API reference: - [Plot](/docs/reference/python/plot.md) — the `Plot` class and table subclasses. - [Mail](/docs/reference/python/mail.md) — the `Mail` class, attributes and the sections API. - [Utilities](/docs/reference/python/utilities.md) — selectors and helper classes. --- # Reference ## Terminology ### OverviewNovem is a fairly complicated platform and to make sure we're all talking about the same thing we've created a set of technical terms with explicit meaning.This page provides a list and definition of the important technical terms and their definition.### Terminology#### FQNP (Fully Qualified Novem Path)The fully qualified novem path is how we refer to objects and entities within the novem platform. The FQNP gives a 100% unique string representation of all resources on the novem platform.FQNPs are designed to be typeable, URL friendly, hierarchical and intuitive.Novem resources are owned by either users or organisations, as such there are two root nodes in FQNP, either `/u/` or `/o/`.By convention all partial FQNPs should alternate between a general and a specific part with the general part listing the available options (conditioned on access). As such `/u/` should list all users and `/u/username/` should list all resources available for `username`.Below we use the example of a plot variable as a fairly complex FQNP, but other visualisations, jobs and repos exhibit similar patterns.For example, to reference a variable published on a novem plot you could reference it as follows\:/u/novem\_demoThe novem user Plot IDs are not globally unique, only usernames and organisations are. Therefore all FQNPs start with either `/u/username` or `/o/orgid`. `/u/` or `/o/` on their own refers to all users or all orgs (that you can see)./p/price\_chartThe specific chart Plot IDs are unique per user so this part selects a specific plot owned by this user. For mails, grids or docs use their respective 1-letter codes (`/m`, `/g`, `/d`). Jobs and repos live under the `/code/` namespace as full words (`/code/jobs/:job`, `/code/repos/:repo`) rather than a single-letter code. Similar to `/u/` using just the `/p/` will refer to all the user's accessible plots./v/close\_priceThe chart variable The lowest level novem offers is individual data points on a vis/repo/job. These variables are individually set and globally accessible. With no further information the default behavior is to color by a linear gradient interpolated between the two colors based on the underlying values in the filtered region.#### ShortnameEvery visualisation, repo, and job also gets a **shortname**: a compact, globally unique id (unlike an FQNP, it doesn't need the owner prefix). You'll find it at each resource's `shortname` path, and it's what the public share links and the `/i` alias below are keyed on.### Public read aliasesAlongside the `/v1/...` management tree, novem exposes a set of read-oriented aliases. They serve JSON and pre-rendered output directly, without the filesystem-style navigation, and honour the same access rules. Public resources work without a token. Everything else needs one with read access.| Path | Description | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/v1/u/:user` | A user's public info and the visuals you can see, under the one-letter codes (`p`, `g`, `m`, `d`, `v`) | | `/v1/u/:user/img` | The user's avatar — what embeds and link previews point at | | `/v1/u/:user/card` | The user's profile card | | `/v1/u/:user/banner` | The user's profile banner | | `/v1/u/:user/f` | The user's activity feed | | `/v1/o/:org` | The organisation equivalent — `card`, `banner`, `img` and the org's visuals under `p` / `g` / `m` / `d` / `v` | | `/v1/i/:shortname` | Any visualisation by its [shortname](#shortname) — renders (`img`, `pdf`, `pptx`, `csv`, `xlsx`, `txt`, `ansi`, `mhtml`), `assets` and vars (`v`), without needing to know its type or owner | | `/v1/t` | Your [tags](/docs/reference/api/tags.md), expanded | | `/v1/t/:tag` | A category tag's public listing — the discoverable `=`-prefixed categories |### Reporting abuse| Verb | Path | Description | | ------ | ----------- | ------------------------------------------- | | `POST` | `/v1/abuse` | Report a user or piece of content for abuse | ## Authentication The novem API authenticates with a **bearer token**. You send it on every request as an `Authorization: Bearer ` header (the CLI and python library do this for you once you've logged in). A few endpoints (registration, login, and the e-mail challenge) are public, so you can bootstrap an account and a token in the first place. Every path below also answers `OPTIONS` with the verbs valid for your token. ### Getting a token | Verb | Path | Auth | Description | | ------ | -------------- | ------ | -------------------------------------------------------------------------- | | `POST` | `/v1/register` | public | Register a new user account | | `POST` | `/v1/auth` | basic | Log in with username + password; creates a token and sets a session cookie | | `POST` | `/v1/token` | basic | Create a new named API token; the token value is returned in the response | | `GET` | `/v1/token` | token | Verify credentials; returns `200` if the token is valid | | `POST` | `/v1/logout` | token | Invalidate the current session | > **Note:** the token value is shown **once**, when it's created. Store it > somewhere safe. You can't read it back later, only revoke it and issue a new > one. The CLI keeps it for you in `~/.config/novem/novem.conf`. #### Giving a token an expiry By default a token lasts until you revoke it. `POST /v1/token` takes two optional fields to bound it instead. Supply **at most one** of them; supplying both is rejected with a `400`. - `expires_in` — a **duration** from now: either an integer number of seconds, or a string like `"24 hours"` or `"7 days"`. - `expires_on` — an **absolute** timestamp, e.g. `"2026-09-01T12:00:00Z"`. ```bash curl -u you:password https://api.novem.io/v1/token \ -d '{"token_name": "ci-deploy", "token_description": "deploy pipeline", "expires_in": "7 days"}' ``` The response echoes the resulting `expires_on` as a UTC ISO timestamp, or `null` for a token that never expires: ```json { "status": "Success", "token": "novem_...", "token_name": "ci-deploy", "expires_on": "2026-08-25T09:14:03Z" } ``` The expiry has to be a finite moment in the future. A past timestamp, a zero or negative duration, and `infinity` are all rejected with a `400`. A token that never expires is spelled by omitting both fields, not by an infinite one. ### The e-mail challenge Some flows (registering, or verifying an e-mail address) send a code or link to your inbox and ask you to prove you received it. | Verb | Path | Auth | Description | | ------ | --------------------- | ------ | ------------------------------------------------------------------------------------------------------- | | `POST` | `/v1/admin/challenge` | public | Answer a challenge by its uid (knowing the uid from the e-mail *is* the proof, so no token is required) | ### Who am I | Verb | Path | Description | | ----- | ------------ | --------------------------------------------------------------------------------------------------- | | `GET` | `/v1/whoami` | The user the current token authenticates as; useful for confirming which account a token belongs to | ### Managing your tokens Your tokens live under `/v1/admin/tokens`, addressed by name. This is where you audit and revoke them, for instance to roll a token that may have leaked or to clean up ones you no longer use. | Verb | Path | Description | | -------- | ------------------------------ | ----------------------------------------------------- | | `GET` | `/v1/admin/tokens` | List your tokens | | `GET` | `/v1/admin/tokens/:token` | A token's folder | | `GET` | `/v1/admin/tokens/:token/info` | Token metadata: name, creation time, expiry, last use | | `GET` | `/v1/admin/tokens/:token/log` | That token's activity log | | `DELETE` | `/v1/admin/tokens/:token` | Revoke the token | Every token in the list carries an `expires_on`, `null` if it never expires. #### Expired tokens stay in the list An expired token stops working the moment it lapses, but it does not disappear. Nothing sweeps it up in the background. The row is your own record of the credential: what it was named, what it could reach, and when it lapsed. It stays listed until you `DELETE` it yourself, exactly as you would revoke a live one. > **Note:** a token in your listing is not necessarily a token that works. Check > `expires_on` against the clock, or read the `Expired` field on the token's > info card. `/v1/admin/tokens/:token/info` still answers for a lapsed token, and it reports the two states separately: ```text Expires on: 2026-08-01 09:14 Active: Yes Expired: Yes ``` `Active: No` means you revoked it. `Expired: Yes` means it ran out on its own. Both answer `401` on use, but they send you to different fixes. ### See also - [API overview](/docs/guides/api.md) — the request shape, verbs and permissions. - [Terminology](/docs/reference/api/terminology.md) — usernames, FQNPs and shortnames. - [Profile](/docs/reference/profile/overview.md) — your account's identity and settings. ## Tags ### Overview Novem offers a set of basic tagging features to help you organise your data. The tags come in three types: - `system` - Tags predefined by novem that you cannot control, these are for things such as favourites and likes. - `user` - Your personal `+`-prefixed tags, private to you, for you to define and use. - `category` - Shared `=`-prefixed tags that publicly categorise your own visuals. The tags are exposed in the API at a top level `tags` endpoint as well as under each of the items that you can apply the tag to, such as [plots](/docs/guides/plot.md), [mails](/docs/guides/mail.md), grids and [repos](/docs/guides/repos.md). > **Note:** Novem user tags are personal, only you can see your tags and they are not shared with or across users. Below is an overview of the main tag endpoints. ```bash . ├── admin │ └── tags │ └── +user_tag │ ├── color │ ├── description │ └── name ├── tags │ ├── +user_tag │ │ ├── code │ │ │ ├── jobs │ │ │ └── repos │ │ └── vis │ │ ├── docs │ │ │ └── @colleague~readme_doc │ │ ├── grids │ │ │ └── @colleague~layout │ │ ├── mails │ │ ├── plots │ │ │ └── @other_user~other_plot │ │ └── views │ └── fav │ ├── code │ │ ├── jobs │ │ └── repos │ └── vis │ ├── docs │ ├── grids │ ├── mails │ │ └── @some_user~some_mail │ ├── plots │ │ ├── @other_user~other_plot │ │ └── @username~plot_name │ └── views └── users └── other_user └── vis └── plots └── other_plot └── tags ├── +user_tag └── fav ``` ### Tag types As mentioned there are three types of tag on the novem platform: user tags, system tags and category tags. #### User tags The most common tag you will use are probably user tags. Usertags must always start with a `+` followed by a lowercase letter and then one or more lowercase letters, numbers, `-` and `_`. Examples of valid tags: - `+my_user_tag` - `+cat5` - `+sen-dev-stack` Examples of invalid tags: - `abc` - `+5cat` - `+long_A` #### System tags The novem system tags are special and only a few of them exist. They are defined by novem and not possible to change or modify. There are currently 8 system tags: - `fav` a way to favourite a vis - `like` a way to like a vis - `ignore` hide a vis from your feed - `wip` mark a vis as work in progress (keeps it out of your own feed) - `archived` archive a vis - `follow` follow a vis for comment and data activity - `pin` feature one of your own plots on your user feed; at most the four most recently pinned plots are promoted - `hide` keep one of your own assets out of lists, search, discovery and feeds `hide` does not change an asset's access rules. Anyone who could already open the asset can still use its direct URL, and GraphQL exposes `hidden: true` on a direct lookup. Hidden assets remain visible in the `hide` tag's own listing so you can find and unhide them; they stay out of every other tag listing. Applying or removing `pin` and `hide` uses the same per-item `PUT` / `DELETE .../tags/:tag` endpoint as every other tag. #### Category tags Category tags start with a `=` and, unlike private user tags, are **shared and publicly visible**. They let you classify your own visuals into common, discoverable categories. - They can only be applied to **your own** visuals. - A visual may carry at most **5** category tags. - The name is global — everyone sees the same `=finance` category. Naming follows the same rules as user tags (a lowercase letter, then lowercase letters, numbers, `-` and `_`), but with `=` instead of `+`: e.g. `=finance`, `=q3-report`. ### Tagging a vis To tag a novem vis you simply `PUT` the tag in the respective vis's tags folder. User tags will be automatically created if they don't already exist. `PUT` `/v1/vis/plots/test_plot/tags/+important` For example, the above tags your own plot called `test_plot`, with your user tag `+important`. `PUT` `/v1/users/my_colleague/vis/plots/kpi_report/tags/+important` And this is how you tag your colleague's`kpi_report` plot with the same tag. To tag an e-mail you would simply perform the same action on the e-mail endpoint. `PUT` `/v1/vis/mails/daily_kpi_report/tags/+important` ### Listing your tags To list your tags you will find them available at the top level `/tags` endpoint. Simply navigate the structure and you'll find all visualizations and other items under their respective tags. ### Customization You may further customize your user tags with colors, names and descriptions. This is done with `POST` to the respective endpoints under the admin structure. ```bash └── admin └── tags └── +important ├── color ├── description └── name ``` For example, if you were to create a new tag `+important` to tag things that were important to you, you could customize it as follows. `POST` `/v1/admin/tags/+important/color` ``` orange-500 -- you can choose any novem color or a hex code ``` `POST` `/v1/admin/tags/+important/name` ```text Important ``` `POST` `/v1/admin/tags/+important/description` ```text Key business indicators ``` ### Endpoint reference Every path below also answers `OPTIONS` with the verbs valid for your token. #### Browsing tagged items The `/v1/tags` tree is read-only; it lists what each tag is applied to, grouped by resource type. Items shared with you appear under their owner's [FQNP](/docs/reference/api/terminology.md) (e.g. `@colleague~report`). | Verb | Path | Description | | ----- | -------------------------------------------------- | ------------------------------------------------- | | `GET` | `/v1/tags` | List your tags (user, system and category) | | `GET` | `/v1/tags/:tag` | The tag's contents, grouped into `vis` and `code` | | `GET` | `/v1/tags/:tag/vis/{plots,grids,mails,docs,views}` | Tagged visuals of each type | | `GET` | `/v1/tags/:tag/code/{jobs,repos,spaces}` | Tagged code resources of each type | Tagging and untagging an individual item happens on **that item's** `tags` folder, not here (e.g. `PUT /v1/vis/plots/:plot/tags/:tag`). See the per-resource API pages (the `Sharing, tags and threads` section). #### Managing your tags User-tag metadata lives under `/v1/admin/tags`. These endpoints only apply to your own `+` user tags; system tags can't be edited. | Verb | Path | Description | | ------------------------- | --------------------------------- | ----------------------------------------------------------------------- | | `GET` | `/v1/admin/tags` | List your user tags | | `PUT` | `/v1/admin/tags/:tag` | Create a user tag (also created implicitly the first time you apply it) | | `GET` | `/v1/admin/tags/:tag` | The tag's metadata folder | | `DELETE` | `/v1/admin/tags/:tag` | Delete the tag and remove it from everything | | `POST` / `GET` / `DELETE` | `/v1/admin/tags/:tag/color` | A novem color name or hex code | | `POST` / `GET` / `DELETE` | `/v1/admin/tags/:tag/name` | Display name | | `POST` / `GET` / `DELETE` | `/v1/admin/tags/:tag/description` | Longer description | ## Social ### Overview Novem offers a basic set of social features to help you connect with and stay up to date on other novem users. The primary purpose of the social features is to populate the `feed`, the primary discoverability feature of the novem platform. You primarily manage the social features under the `/admin/social` endpoints with feed entries showing up under the top level `/feed` endpoint. Below is an overview of the API endpoints associated with the social features. ```bash ├── admin │ ├── invites │ │ └── @eve │ │ └── accept │ └── social │ ├── connections │ │ ├── alice │ │ └── bob │ ├── followers │ │ └── charlie │ ├── following │ │ ├── charlie │ │ └── mike │ └── ignore │ └── dave └── feed ├── @charlie~plot~daily_weather ├── @charlie~plot~email_use ├── @mike~mail~px_report └── @mike~plot~power_prices ``` ### Feed The feed is the primary discoverability feature at novem and is curated for you by the platform. Whilst the exact algorithm is not disclosed and changes often, the primary input to your feed is content produced by other users in your network. The feed is accessible at the top level of the API under the `/feed` end point and contains a list of links of the following format: `@username~vis_type~vis_name`. The entities link to the respective vis by the user. You will only find one reference to a specific vis in your feed, but the vis can move up or down in your feed if it gets updated or other relevance scores change. When viewing a user you can find that users chronological feed under the `feed` endpoint. e.g. `/users//feed`. ```bash └── users └── └── feed ├── @example~mail~test_mail ├── @example~plot~test_plot_1 └── @example~plot~test_plot_2 ``` User feeds are always chronological and can be thought of as an activity feed. #### Your network Your network on novem is made up of 3 classes of connections: - Users that you are connected with - Users that you follow - Users that you share a group or organisation membership with In addition your extended network can consist of 2nd and 3rd degree connections as well as be inspired by things you have liked or favourited. > **Note:** In all feed folders the expected sort order is ascending by the `order` property. All consumers of the novem > API are responsible for sorting by this property if you want to consume the feed output in its intended order. #### Publishing By default, all assets produced on the novem platform that a user can access through the default permission structure (public or shared via group) is eligible for inclusion in feed. To keep an in-progress visualisation out of **your own** feed (because it's not ready, or you simply don't want to see it there yet) you can [tag](/docs/reference/api/tags.md) it with `wip`. The `wip` tag hides the item from your own feed view; it does not remove it from other users' feeds. A user will never see a visualisation in their feed that they don't have access to, so private assets are always private. ### Relationships Novem offers two primary social constructs, a way to `connect` with and a way to `follow` other users. The connection is an invitation that has to be accepted by the other user, whereas the follow is a one-way action. Connections and follows does not grant any additional access or other features at this moment, but might be expanded in the future. To see the information from a user the user will have to make that information public or share it with a group that you both are a member of. #### Connections Connections is the highest order of `1:1` relationships offered by novem. A connection has to be accepted on both sides and who your connections are will show up on your public profile. (People with private profiles will not show up unless they have a strong connection with the person viewing your profile). To make a new connection simply PUT the username you want to connect with to the connection group `PUT` `/v1/admin/social/connections/` Once you have initiated a connection the user will get an invitation to connect which they will have to approve by posting a positive value to the invitation. `POST` `/v1/admin/invites/@/accept` ```text Yes ``` Until a user accepts your invitation to connect the user will show up in the list of your connections with an `I-` *(capital I dash)* prefix. (Examples below) ```bash └── admin └── social └── connections ├── I-alice ├── bob └── eve ``` You can at any point remove your connection with a user by issuing a `DELETE` against an existing user invite or endpoint. `DELETE` `/v1/admin/social/connections/` > Connecting with people are a privilege and excessive invite spamming, frequent retries or other unwanted behaviour may > result in your ability to connect being curtailed. #### Following Unlike connecting, follow is a lighter touch, one-way action. You may follow or un-follow anyone you want at any time. In addition, following someone does not require their approval, and as such no invite or acceptance is required. Managing who you follow is very similar to managing your connections, you use `PUT` to follow and `DELETE` to un-follow. `PUT` `/v1/admin/social/following/` `DELETE` `/v1/admin/social/following/` #### Followers Followers is a list of people following you and is provided for informational purposes only. We don't currently support the ability for you to remove your followers or moderate the list in any way. Your followers don't interact with you in any way beyond having access to your public assets in their feed. #### Ignore Ignore lets you manage a list of users you do not want to relate to. When a user is in the ignore list you will not receive invites, messages, notifications or see other information left by the user. Ignored users will also never show up in your feed. You manage your ignore list very similar to your following list, simply `PUT` to add a user and `DELETE` to remove a user from your ignore list. `PUT` `/v1/admin/social/ignore/` `DELETE` `/v1/admin/social/ignore/` ### Invites Invites are managed under `/v1/admin/invites`. The same tree holds three related things: the connection invitations you've received, the personal e-mail invitations you send to bring new people to novem, and (on paid plans) reusable invite URLs. A `GET` on `/v1/admin/invites` lists everything together: inbound connection invites, your pending personal invites, and your invite URLs. #### Connection invitations When someone invites you to [connect](#connections), the invite appears under `/v1/admin/invites/@`. Accept it by posting a positive value to its `accept` file (shown in [Connections](#connections) above). The same pattern handles inbound invitations generally. #### Personal invitations A personal invitation brings a new person to novem by e-mail. The e-mail address *is* the resource id: | Verb | Path | Description | | ---------------- | --------------------------------- | ------------------------------------------------------------------ | | `PUT` | `/v1/admin/invites/:email` | Send an invitation to that e-mail address | | `DELETE` | `/v1/admin/invites/:email` | Revoke a pending invitation | | `GET` | `/v1/admin/social/invites` | List invitations that have been **accepted** (your invite history) | | `PUT` / `DELETE` | `/v1/admin/social/invites/:email` | Equivalent send / revoke under the social tree | Pending (not-yet-accepted) invitations show up in the `GET /v1/admin/invites` listing. Once accepted they move to `/v1/admin/social/invites`. Each send draws down your account's invite quota. #### Invite URLs An invite URL is a named, reusable signup link, useful for a meetup or a campaign where you don't know the addresses up front. You create a **handle**, novem mints an opaque public URL, and anyone who visits it submits their e-mail and joins as your invitee. > **Note:** invite URLs require a paid subscription. Free accounts can't create > them, while **basic**, **premium**, and **enterprise** each allow up to **3** > active handles. | Verb | Path | Description | | -------------- | --------------------------------------- | ------------------------------------------------------------------------------------- | | `PUT` | `/v1/admin/invites/:handle` | Create an invite-URL handle; optional JSON body `{quota, expiry, description}` | | `GET` | `/v1/admin/invites/:handle` | The handle's folder | | `DELETE` | `/v1/admin/invites/:handle` | Deactivate the handle | | `GET` | `/v1/admin/invites/:handle/info` | Aggregated state: active, url, quota, expiry, description, pending and accepted lists | | `GET` | `/v1/admin/invites/:handle/url` | The public URL | | `POST` / `GET` | `/v1/admin/invites/:handle/quota` | Per-URL signup cap (bare integer) | | `POST` / `GET` | `/v1/admin/invites/:handle/expiry` | `never`, an interval, or an absolute timestamp (in your profile timezone) | | `POST` / `GET` | `/v1/admin/invites/:handle/description` | Free-form owner-facing context | | `POST` | `/v1/invite/:token` | **Public, no auth** — an external visitor submits their e-mail here to accept | Each landed signup decrements both the URL's own `quota` and your account-level invite quota, so a URL can't outrun either limit. ### Interaction Currently there are two ways that you can interact with other users published visualisations. You can either `like` or `favourite` them. This is done using the novem [tag](/docs/reference/api/tags.md) functionality and behaves just as normal tags. The only difference is that aggregate like and favourite statistics are reported on various visualisations. To like or favourite a visualisation just follow the instructions in the [tags](/docs/reference/api/tags.md) documentation. #### Hide If you see anything in the feed you do not want to see again, you can also ignore it by tagging it with the `ignore` tag. ## Slicing The Novem slicing instruction is modelled on the [python array slicing](https://python-reference.readthedocs.io/en/latest/docs/brackets/slicing.html) notation, but expanded with a few quality of life enhancements. The goal of slicing is to provide an easy way to subdivide your data for applying styles and formatting — and the SAME notation, with the same meaning, is used by every config file: table cells, chart colors, mark types, labels, marks, facets and legends. In addition for the primary slicing function, we also allow the user to supply a comma separated list of indices starting at zero. (Negative indices count from the end in colon slices such as `-1:` and as a single bare index, but not inside a comma-separated list.) ### Syntax The novem slicing syntax is created to operate on sub-sections of tables. To achieve this we need two slices, one for rows and one for columns. These two slicers together is called a selector. Any styles using selectors apply to the intersection of the two slices — the block of cells where the selected rows and columns cross. #### Slicers > **Note:** > > Please be aware that slicing instructions cannot contain any spaces. The slicing operation consists of one or more integers separated by a colon `:`. The first number denotes the starting position (counting from zero), the second the ending position and the third the step (how many items to skip). The integer values can be skipped to indicate start or end of a range. 0 Start index Zero based index on where to start, if left empty the slice will start at index 0 (start of row/column). A comma separated list of indices can be supplied instead, but then the remainder of the instruction must be removed. : Position separator Splits up the start and end sections. -1 End index Which index to stop at. This can be an explicit value (`5`) or relative value from the end of the range (`-5`). If this element is not specified, the slice defaults to the last element of the range. **Note:** The last element of a range is `-1`. : Step separator Splits of the end and step sections. 2 Step value This value indicates how many steps to take between each value, by default a step of 1 is assumed, selecting every element. A step of 2 would thus select every other element, and so on. A negative step selects nothing — in every index space, the axis tick slot included; junk never broadens a selection. #### Selectors For a complete selector, a slice is specified twice, once for rows and once for columns. The instructions are separated with a space. Below are a few examples of selectors along with some explanations. ```novem-color-border -- select all rows and all columns : : -- select first row and all columns 0 : -- select last row and all columns -1 : -- select first three rows 0:2 : -- 2 is the third element (zero-indexed), and the stop is INCLUSIVE -- select last three rows -3: : -- select a 3x3 matrix in the top left corner of the table 0:2 0:2 :2 :2 -- the zero is implied if omitted -- select every other row in the table ::2 : -- select the first and last row 0,-1 : -- select row 4,5,6 3,4,5 : -- remember zero index ``` #### One index space: the raw file Every selector addresses the **raw uploaded file**: `(0,0)` is the very first value of your csv. Row `0` is the header line, column `0` the leftmost column — on a table you can style them like any other cell. Charts use the exact same coordinates. Column `0` is the x /category column and row `0` the header line; neither carries a paintable mark, so a selector that lands there simply paints nothing. In practice: the first series is column `1`, the first data point row `1`, and a line that works on a table means the same cells on a chart of the same csv. Pie and donut slices are rows — `2 :` addresses the second slice. Two spaces are deliberately not the csv, because they only exist after layout: facet strip-label cells and per-panel margins address the physical panel grid, and the axis grammar's third slot addresses positions in the generated tick sequence. They still use the same slicer vocabulary. #### One slicer A line with a single slicer addresses **columns**: `revenue b` bolds the revenue column, `1 red` colors the first series. Two slicers are always rows × columns, and the two-slicer reading wins whenever both leading tokens parse as slicers. #### Header names A column slicer can use the column's **name** instead of its index: `revenue`, `big_cat` (spaces read as underscores, case-insensitive). Names work inside ranges and lists — `revenue:costs`, `q1,q3` — and resolve to the column's raw position. Digits always win: a header literally named `2024` never hijacks the positional selector `2024`. Row selectors are always positional. #### Edge semantics - **Stop is inclusive**: `0:2` is three rows (the pandas `.loc` rule). - **Overflows are inclusive**: `:1000` on 100 rows is all 100, `-500:` on 50 rows is all 50. Only a window entirely outside the axis — `100:200` on 5 rows, or a reversed range like `3:1` — selects nothing. - **Negatives count from the end**: `-1` is the last row; an over-negative single index (`-99` on 3 rows) selects nothing. - **Junk selects nothing.** A selector that fails to parse drops its line — it never degrades to "everything". In comma lists, invalid items drop individually and the rest survive. #### Line markers: `+` and `-` In the chart grammars ([type](/docs/reference/plot/config/type.md), labels, marks, [colors](/docs/reference/plot/config/colors.md)) and the table styling and format files, a `+` or `-` as the FIRST character of a line, followed by a space, is a line marker: - `+ [overrides]` restates the nearest preceding line over a new selection — `l:%v le:t` then `+ 3` puts the same label on another series. - `- ` erases those cells from every line above it, and later lines still paint into the hole. Attached signs always belong to the slicer: `-1 red` colors the last column, `- 1` erases column 1. Table styling and format files take the same markers: a bare `-` or `+` as the first token of a line is always a marker there too, and the historical tolerant reading of a bare leading `-` is retired. On a `+` line whose tokens are all slicer-shaped the tokens read as the new selector; when a slicer-shaped payload is meant (the align dash), write the row slicer explicitly (`+ : 1 -`). ### Try it out! Play around with a live demo of the slicer syntax: *Interactive demo — see the docs site.* ### Examples Below are a couple of examples showing how the slicer selection can be used to apply different styles to a novem table. #### Slicers and steps This example shows a static color pattern reminiscent of a picnic blanket using three shades of blue and setting the background color. *Embedded visualisation:* `/u/novem_demo/p/color_config_ix_picnic` We use four selectors and leverage the step instruction. First color alternating rows starting at different offsets, then add alternating columns. The selectors are applied in order, so later entries take precedence. `text` `/config/colors` ```novem-color-ix -- Create a "picnic blanket" using 4 shades of blue ::2 : bg blue-100 -- color every other row starting at 0 1::2 : bg blue-200 -- color every other row starting at 1 1::2 1::2 bg blue-300 -- color every other col starting at row 1 col 1 ::2 1::2 bg blue-200 -- color every other row starting at row 0 col 1 ``` #### Index lists Here we highlight a hierarchical structure in our table by coloring rows in descending shades of gray. This is a common strategy when wanting to preserve space. *Embedded visualisation:* `/u/novem_demo/p/color_config_ix_hierarchy` Unlike in the picnic example above, here we explicitly choose which entries to highlight with a comma-separated list of rows. `text` `/config/colors` ```novem-color-ix -- Color rows by "hierarchy" 1 : bg gray-500 -- world 2,5,8 : bg gray-400 -- region 1 3,6,9,12,19 : bg gray-300 -- region 2 ``` ## Vars ### Overview Novem is all about sharing live knowledge, and sometimes you want to share more than just the visual output itself. Think metadata such as when it was last updated or if there were any errors processing the data. Perhaps there are things about the visual that you want to reference individually, such as what the latest closing or open price of your time-series chart was. This is where novem variables, or `vars` as we call them, come into play. `vars` can be attached to any of the novem visuals ([plots](/docs/guides/plot.md), [mails](/docs/guides/mail.md), grids, docs and views) as well as jobs and repos, and provide additional information about the entity. These `vars` can then later be referenced in comments or descriptions as well as in your mails and docs with a simple reference `{/u/novem_demo/p/price_chart/closing_price}` (gives you the formatted closing price variable for @novem\_demo's `stock_price` chart). The API structure of `vars` looks as follows (using plots as an example): ```bash stock_price => plot id ... ├── vars │ ├── closing_price => variable name │ │ ├── about => short description │ │ ├── format => numeric or date format │ │ ├── type => type of variable, relative, number, date, text │ │ └── value => value of variable │ ├── dtd_return │ │ ├── about │ │ ├── format => format of relative value │ │ ├── type => relative variable type │ │ ├── threshold => threshold visible when type is relative │ │ └── value => numeric relative value │ └── ticker │ ├── about │ ├── type => text type, shows only about and value │ └── value => text string ... ``` ### Using variables Using novem variables is fairly simple. In text fields that support [simple novem markdown](/docs/reference/markdown/overview.md#simple-markdown) simply use curly brackets to wrap a [FQNP](/docs/reference/api/terminology.md#fqnp-fully-qualified-novem-path) to the variable id. For example, to reference the above `dtd_return` you could write `{/u/novem_demo/p/stock_price/v/dtd_return}` and get something like `▲ +24.50%`. #### Comments & Chats One of the most common ways to use `vars` are in comments and chats. When discussing a technical subject it's often helpful to reference hard data such as numbers of visitors or price performance inline in the conversation. Novem variables let you reference live prices, performance measures or numbers directly in messages and comment threads. #### Mails When building data-driven e-mails it's nice to be able to reference meta-variables such as recipient name or fund returns (if creating content for multiple funds). By moving these values into `vars` attached to the e-mail the templates can be cleaner and better structured. Pulling `vars` out of embedded charts also makes it possible to write rich content without having to independently source the chart data. #### Documents Similarly to mails, documents are also a common place to leverage `vars` both for document meta-information as well as for enriching content around embedded charts and data. ### Formatting variables Novem variables offer a limited set of formatting instructions. Primarily the formatting job is left to the consumer of the variables so local styling considerations (such as output format and render type) can be taken into account. We do however allow you to express certain meta-attributes about your vars to guide presentation. This is done by defining the `type` of the variable followed by relevant configuration options. #### Text The text variable is the simplest of the variables: ```bash vars └── ticker => variable id ├── about => description ├── type => text type └── value => text string ``` It consists of just one control option, `value`, that will be embedded verbatim when used. #### Date Dates are slightly more complicated, allowing the user to express formatting rules for the date. (The advantage here is that users can override date formatting on embed.) ```bash vars └── ticker => variable id ├── about => description ├── type => date type ├── format => date format string └── value => date value ``` Date input values are expected to be in YYYY-mm-dd format and the `format` endpoint supports the standard novem date formatting syntax. #### Numeric Similar to date, numeric is just a value with formatting instructions: ```bash vars └── ticker => variable id ├── about => description ├── type => numeric type ├── format => number format string └── value => numeric value ``` Any number is accepted with the `format` endpoint supporting the standard novem number formats. #### Relative Relative is by far the most complicated of the vars. The idea with a relative variable is that it can show changes relative to a threshold: ```bash vars └── ticker => variable id ├── about => description ├── type => relative type ├── format => format string ├── threshold => threshold value └── value => numeric value ``` The most important part here is threshold. Values below the threshold will be indicated as a negative relative value, usually with red colors and a downward arrow. Values above the threshold will be equally positive. ## Files ### Overview Novem is heavily inspired by the unix philosophy of ["everything is a file"](https://en.wikipedia.org/wiki/Everything_is_a_file). As such our API is implemented as a hierarchical file system. As part of this we wanted to make sure that these virtual files supported sensible extension and content expectations. This document provides a short overview of the kind of files you can expect to find in the novem API and how to interact with them. ### Inputs All of our interactions are performed by standard HTTP verbs over HTTPS. If an operation requires submitting data, this is done by providing utf-8 encoded plain text as the body of the post request. There are a few exceptions, such a registering user accounts, but those are marked as `system_dir` and `system_file` and can safely be ignored when interacting with the API. #### Value spaces You always write a plain-text body. The **value space** (what counts as a valid value) depends on the node. Each writable file is one of a few kinds: - **Text** — free-form text: display names, summaries, descriptions, and similar fields. Some text fields are *normalised* on write (leading and trailing whitespace trimmed, zero-width and directional Unicode characters stripped) so a pasted value behaves predictably. How a text value is *rendered* varies. See [Markup](#markup) below. - **Boolean** — a yes/no toggle. The accepted true values are `yes`, `y`, `yeah`, `yup`, `true`, `t`, `ok`, `ye` and `on`; the false values are `no`, `n`, `nope`, `false`, `f` and `off` (all case-insensitive). Anything the API doesn't recognise is treated as **false** (and logged), so prefer a plain `yes` or `no`. - **Option** — one value from a fixed set. A value outside the set is **rejected**, and the existing value is left unchanged. The valid set is listed on the node's own reference page. Notification settings, for example, accept `ignore`, `info`, or `important`. - **Structural** — a few folders interpret the *path* rather than a body: `shared/:group` takes a share target (`public`, `@user~group`, `+org~group`), `tags/:tag` takes a [tag](/docs/reference/api/tags.md) name, and a [var](/docs/reference/api/vars.md) carries a typed value with its own `type`/`format` fields. Scalar controls are safe to edit as ordinary text files: the API removes the complete trailing line-ending sequence (including LF and CRLF) before it validates or stores a text, boolean, or option value. Content-bearing files, such as documents, code, CSS, and plot data, preserve their body, including a final newline. #### Configuration values A visualisation's `config/*` keys behave differently from the API-validated options above. They're stored verbatim as text and interpreted later by the rendering pipeline, which **falls back to a sensible default** for a value it doesn't understand rather than rejecting the write. A typo in `config/type` therefore yields the default render, not an error. The recognised set for each key lives on the per-product config reference (e.g. [plot config](/docs/reference/plot/config/overview.md), [grid layout](/docs/reference/grid/layout.md)). A handful of value vocabularies recur across those keys: - **Colors** — a novem theme color name (`gray-300`, `ok`, `bad`, `warn`), one of the `tab10`/`tab20` palette slots, an XKCD color name, or a `#rrggbb` hex code. Many color keys also accept a leading `* ` to mean "every series", e.g. `* gray-300`. See [theme colors](/docs/reference/theme/colors.md). - **Sizes** — a percentage (`40`) or an explicit pixel value (`120px`). - **Booleans / enums** — the same `yes`/`no` and fixed-set conventions as above, but coerced-to-default rather than rejected. #### Markup Most text is shown verbatim. A few fields are rendered, at one of three levels. The level is fixed per field: - **Plain** — names, summaries, ids, var values and most config values are shown as-is, never interpreted. - **Description markdown** — resource `description` fields and plot/grid commentary go through a *medium* markdown dialect: inline formatting (bold, italic, code, strikethrough, superscript), fenced code blocks, simple tables, KaTeX math and [variable](/docs/reference/markdown/variables.md) embeds. Raw HTML in the source is escaped, not rendered. - **Full novem markdown** — the **body of documents and e-mails** is the complete novem markdown dialect: sections, layout, visualisations and the rest, documented under [markdown](/docs/reference/markdown/overview.md). Rendered HTML is sanitised before it is served. Scripts, event handlers, and unknown tags are stripped, and URLs are limited to `http`, `https`, `mailto`, and `data`. The one place you supply HTML directly is a render header `title` or footer `footnote`. These accept a small inline subset (`img`, `b`, `strong`, `i`, `em`, `span`, `br`); put any CSS in `config/render/styles` instead. ### Size limits Each writable file has a maximum size, enforced on the `POST`/`PUT` body. The limit is measured in **characters** (Unicode code points), not bytes — content with multi-byte (non-ASCII) characters reaches the cap with fewer bytes. An oversized write is rejected with a `413` response (`Request is too large. This endpoint only supports up to N characters`) and nothing is stored. | File | Endpoint | Limit | | ---------------------------- | ---------------------------------------------------------------------- | ----------------------------- | | Plot data | `/v1/vis/plots/:plot/data` | 2,000,000 characters (\~2 MB) | | Document content | `/v1/vis/docs/:doc/content` | 5,000,000 characters (\~5 MB) | | Custom chart script | `/v1/vis/plots/:plot/config/custom/custom.js` | 5,242,880 characters (5 MB) | | Plot config grammars | `/v1/vis/plots/:plot/config/{axis,margins,facets,colors,labels,marks}` | 10,000 characters each | | Grid layout | `/v1/vis/grids/:grid/layout` | 10,000 characters | | Grid mapping | `/v1/vis/grids/:grid/mapping` | 10,000 characters | | View address | `/v1/vis/views/:view/addr` | 10,000 characters | | Primary organisation code | `/v1/admin/profile/options/primary_org` | 52 characters | | Organisation identity colour | `/v1/admin/orgs/:org/profile/options/{primary_color,secondary_color}` | 16 characters each | Most other control files (names, descriptions, config flags) carry small limits of their own, in the hundreds to low thousands of characters. A job's rendered *result* is capped separately at 100 MB — see the [jobs guide](/docs/guides/jobs.md). ### Output Output formats can vary a bit more than the plain text of the input, but mostly you'll encounter either json for visualisations or dir entries, or plain text for control and data files. The output of visualisations can be presented in more ways than viewing the web presentations or the e-mails, so we also provide a collection of special purpose output formats, like PDF or a pre-rendered image. Their outputs are accessible in two different ways, either by appending the corresponding extension to the [canonical url](/docs/guides/api.md#canonical-url) of our charts or by accessing the file in the individual visualisations `/files` folder. ### Formats Different visualisations support different output formats, both depending on the type of visualisation (mail, plot, grid, doc) and the individual subtypes (table plot vs line plot). If you're curious what types are available you can always list the content of the `/files` folder of the visual, but we've provided a rough overview below as well. Further details are also documented under the sections for the different visualisation type. #### Portable Document Format `pdf` The Portable Document Format, or PDF, developed by Adobe in the '90s is the current gold standard in document sharing. Mostly because it always looks the same and prints pretty well. Many users still prefer to print documents for meetings, analysis and offline reading, and PDF is the go-to format for these use cases. It was important to us that our documents would look good when printing, so we've created a separate rendering pipeline for our PDF documents to make sure we get the best experience. You can control several aspects of the pdf rendering process under the `/config/render` endpoint for the relevant visuals. #### Portable Network Graphics `png` While PDF is the gold standard for documents, PNG is an excellent format for digitally rendered images. Although the customization options for PNG files are not as extensive as those for PDF files, our visuals also provide PNG versions of their output for convenient use in digital environments. #### American National Standards Institute Escape Codes `ansi` Whilst novem was initially intended to let you create web visualisations from the command line, the advent of cool services such as [wttr.in](https://wttr.in) inspired us to try and create an ascii art version as well. If you try to `curl ` of any of our visuals that support an `ansi` output you should receive the ansi version instead. You can also add the .ansi extension to be explicit. The ansi version uses 256-color ansi escape codes and utf-8 box drawing characters. To see if your current terminal and font supports it. try `curl -L novem.io/terminal`. #### Text `txt` The text version is a simplified version the above ansi output, but without colors. ## Sharing ### Overview Novem resources are private to their owner by default. To let other people see one, you share it by creating an entry in its `shared/` folder — one entry per target you share with. This works the same for visualisations (plots, grids, docs, mails) and code resources ([repos](/docs/guides/repos.md) and [jobs](/docs/guides/jobs.md)). ```bash en_letter_frequency └── shared ├── @jones => the user jones, directly ├── @smith~analysts => analysts user group owned by smith ├── +acme~research => research group in the acme org └── public => shared with everyone ``` ### Share targets You add a share by `PUT`ting the target as a file under the visual's `shared/` folder. The leaf name encodes who you are sharing with: - `@username` — a **user**, directly. The user must be visible to you: public, connected to you, or in a group with you. - `@username~group` — a **user group**: the `group` created by `username`. You must be a member of that group. - `+orgname~group` — an **org group**: the `group` belonging to the organisation `orgname`. - `public` — the **world**. Your own account must be public to share publicly. ```bash PUT /v1/vis/plots/en_letter_frequency/shared/@jones PUT /v1/vis/plots/en_letter_frequency/shared/@smith~analysts PUT /v1/code/repos/data_fetcher/shared/@jones ``` A direct share is private to the two of you: only you and the target user ever see that entry in the share listing. The target is notified when the share is created. > **Note:** You cannot share with a whole organisation; a bare `+orgname` > target is rejected with a `403`. For a broader audience than one user, > create a [user group](/docs/reference/api/groups_and_orgs.md) (`@you~group`), > add the people you want, then share with that group. ### Granting more than read access By default a share grants **read-only** (`r`) access — recipients can view the visual but not change it. To grant more, append a final `~`-segment listing the permission letters you want (`r`, `w`, `d`): ```bash -- let the analysts group read and write PUT /v1/vis/plots/en_letter_frequency/shared/@smith~analysts~rw -- let jones read and write PUT /v1/vis/plots/en_letter_frequency/shared/@jones~rw ``` Re-sharing the same target with different permissions updates the existing share; re-sharing with the same permissions is a no-op. > **Note:** In the two-segment `@` form, a second segment made up only of > permission letters (`r`, `w`, `d`, `x`) always reads as permissions for a > direct share. Permission-string names are therefore reserved: creating a > user group named like one (say `rw`) is rejected. ### Removing a share To revoke access, `DELETE` the corresponding entry from the `shared/` folder: ```bash DELETE /v1/vis/plots/en_letter_frequency/shared/@smith~analysts ``` ### Accessing things shared with you Resources shared with a group you belong to appear under that group's `shared/` tree, organised by kind: ```bash /v1/users//groups//shared ├── vis │ ├── plots => @~ entries │ ├── grids │ ├── docs │ └── mails └── code ├── repos => @~ entries └── jobs ``` Each leaf lists one entry per shared resource, named `@~`. You can also access a shared resource directly at its canonical path (e.g. `GET /v1/users//code/repos//branches`) — browse it just like your own, subject to the permissions the owner granted. Resources shared with you **directly** have no group tree; access them at their canonical path. The resource's `shared/` listing shows your own `@you~` entry, which is how a client can tell what you are allowed to do with it. > **Note:** A leaf returns `404` while nothing of that kind is shared with the > group — an empty `shared/vis/plots` is indistinguishable from a missing one. ## Groups ### Overview Novem allows you to create and be part of groups and organisations. This is the principle entity for access management on the novem platform and how various documents and visualisations are shared between users. In this document we will show you: - How to create a new group and invite a user - How to promote the user to an admin role - How to remove a user - How to accept an invite from another user - How to create a group page #### Groups When it comes to access management and sharing novem has the concept of `groups`. Groups can either belong to a user and be a `user group` or an organisation and be an `org group` A user group consists of an @ followed by a username, then a \~ and a group name. An organisation group consists of a + followed by an organisation name, then a \~ and a group name. Neither usernames, organisation names or groupnames may contain a space. User groups are created by `PUT`ing a group name under the `/admin/groups/` folder and organisations by `PUT`ing an organisation name under the `/admin/orgs/` folder. To add a user to a group you simply `PUT` the `username` in the `/admin/groups//roles//` folder where `` is one of `founders`, `admins`, `superusers` or `members`. The user creating a group or organisation is automatically given the `founder` role. The groups of which you are a member are listed under `/admin/groups`, keyed by their group slug (not the display name). Org groups you can see are also surfaced under `/orgs//groups`. ### User Groups User groups are the unit of sharing for individuals. Create one by `PUT`ting a group name under your `/admin/groups/` folder; you become its `founder` automatically. ```bash PUT /v1/admin/groups/analysts ``` A user group is referenced elsewhere, for example when sharing a visual, as `@username~groupname` (e.g. `@alice~analysts`). #### Inviting users Add a member by `PUT`ting their username into one of the group's role folders. The roles, from most to least privileged, are `founders`, `admins`, `superusers` and `members`. ```bash PUT /v1/admin/groups/analysts/roles/members/bob ``` The invited user receives an invitation they must accept before the membership becomes active; until then they show in the role listing with an `I-` prefix (the same pending marker used for [social](/docs/reference/api/social.md) connections). #### Accepting or declining an invite Pending invites are listed under the invited user's `/admin/invites/` folder, keyed by the group reference (`@alice~analysts`). Each invite holds a single `accept` endpoint; `POST` `yes` to it to accept, or `no` to decline: ```bash GET /v1/admin/invites -- list pending invites POST /v1/admin/invites/@alice~analysts/accept -- body: yes (or no to decline) ``` ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: text/plain" \ --data "yes" \ https://api.novem.io/v1/admin/invites/@alice~analysts/accept ``` > **Note:** Text bodies must be sent as `text/plain`. `curl`'s `--data` flag > defaults to `application/x-www-form-urlencoded`, which the API rejects — > always pass `-H "Content-Type: text/plain"` alongside it. #### Promoting users Changing a member's role is the same `PUT`, into a different role folder — to promote `bob` from member to admin: ```bash PUT /v1/admin/groups/analysts/roles/admins/bob ``` #### Removing users `DELETE` the user from the role folder to revoke their membership. ```bash DELETE /v1/admin/groups/analysts/roles/members/bob ``` ### Organisations Organisations are shared parents for groups that belong to a company or team rather than a single user. Create one by `PUT`ting an organisation name under `/admin/orgs/`; as with groups, the creator becomes the `founder`. ```bash PUT /v1/admin/orgs/acme ``` > **Note:** Two constraints apply, both enforced by the API: only > **organisation accounts** can create organisations (a regular account gets a > `403`), and each account can create exactly **one**. Organisation-level roles (membership of the organisation itself) are managed under `/admin/orgs//roles//` using the same four role folders. #### Organisation Groups An organisation's groups live under the org and are referenced as `+orgname~groupname`, e.g. `+acme~research`. They are created and have their members managed exactly like user groups, one level deeper under the org: ```bash # create the research group under the acme org PUT /v1/admin/orgs/acme/groups/research # add a member PUT /v1/admin/orgs/acme/groups/research/roles/members/bob ``` #### Organisation admins see every group An organisation `admin` or `founder` has oversight of every group under the organisation. All of the org's groups appear in their views — the `/v1/orgs/:org/groups` listing, the organisation's groups in GraphQL, and the dashboard — each with its profile and full member roster, even for groups the admin has not joined. A `superuser` is an in-group moderator role and carries no org-wide oversight; like a regular organisation member, they see only the groups they belong to. Oversight is read-only on its own. To administer a group — invite or remove members, edit its profile or options — an org admin must hold a role in the group itself, and may claim one directly, without waiting for an invite, by `PUT`ting their own username into a role folder: ```bash # an org admin claims the admin role on a group under their org PUT /v1/admin/orgs/acme/groups/research/roles/admins/alice ``` The role takes effect immediately, with no invitation to accept, because the admin is exercising the organisation's own authority. This is the intended way to take over a group whose previous administrator has left. Until a role is claimed the group's `role` sub-resource returns nothing, since the admin holds no role in it yet. #### Inbound e-mail address An org group can have its own **inbound e-mail address**. Mail sent to it triggers a [run](/docs/reference/jobs/runs.md) of every [job](/docs/guides/jobs.md) shared with the group; it is not delivered to the group's members. The address is computed and read-only; it takes the form `@.`, for example `research@acme.` (the domain suffix is configured per environment). The address exists only when both of these hold: - the parent organisation has its mail subdomain enabled, and - the group accepts inbound mail. Read it from the group's `email` sub-resource. When the group is not mail-enabled the value is empty: ```bash GET /v1/orgs/acme/groups/research/email -- plain text, "" if not mail-enabled ``` Any member of the group (`member`, `superuser`, `admin` or `founder`) can read it. The same value is returned as the `email` field on the group's info object and as the `email` field on `Group` in GraphQL; both are `null` when the group is not mail-enabled. ### Group & organisation pages A group or org has a profile, the page people see at its public URL. It works like your own [profile](/docs/reference/profile/overview.md): a `profile` folder with text fields and imagery, all written with `POST` (or `PUT`/`DELETE` for imagery) by a `founder` or `admin`. | Verb | Path | Description | | ------------------------- | ---------------------------- | --------------------------------- | | `GET` | `.../profile` | The profile folder | | `POST` / `GET` / `DELETE` | `.../profile/name` | Display name | | `POST` / `GET` / `DELETE` | `.../profile/description` | Description, rendered as markdown | | `POST` / `GET` / `DELETE` | `.../profile/picture/image` | Avatar | | `POST` / `GET` / `DELETE` | `.../profile/picture/banner` | Banner | | `POST` / `GET` / `DELETE` | `.../profile/picture/mask` | Avatar crop (`x,y,r`) | `.../` is `/admin/groups/:group` for a user group or `/admin/orgs/:org/groups/:group` for an org group; orgs themselves expose the same `profile` tree at `/admin/orgs/:org`. #### Organisation workflow activity `GET /v1/orgs/:org/profile/stats/activity` returns a daily workflow-run series for the authenticated caller's readable shares within an organisation. Despite living under the read-oriented `/orgs` tree, this is member activity, not public organisation analytics. The caller must hold an accepted `member`, `superuser`, `admin` or `founder` role either directly on the named organisation or on one of its child org groups. A pending role does not qualify; an unknown organisation or a caller without that relevance receives `404`. The activity itself is narrower than that relevance check: it includes only distinct jobs carrying a readable (`{r}`) share to a child org group under the named organisation in which the caller is an accepted member. A job shared through more than one qualifying group is counted once. There is no parent-organisation content inheritance. A direct organisation member or administrator who belongs to none of its child groups can reach the endpoint but receives a zero-filled series. Public-only jobs, jobs shared directly with the caller, jobs shared through user groups or another organisation, owned-but-unshared jobs, jobs reachable only through a pending group membership and shares without read permission are not included. Only top-level runs are counted; nested child runs are excluded. The response is a dense chronological series ending on the current UTC day and capped at the most recent 371 UTC calendar days. Days without matching runs are present with zero counts. The default response is a plain-text table with flat columns. Send `Accept: application/json` to receive the same data as an array: ```bash curl -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Accept: application/json" \ https://api.novem.io/v1/orgs/acme/profile/stats/activity ``` ```json [ { "day": "2026-08-17", "views": 0, "vdes_created": 0, "mails_sent": 0, "social": 0, "jobs": { "total": 3, "trigger": { "api": 1, "schedule": 1, "email": 1 }, "status": { "success": 2, "failed": 1 } } } ] ``` `jobs.total` is the total number of included top-level runs for the day; `jobs.trigger` and `jobs.status` provide the corresponding trigger and outcome breakdowns. The `views`, `vdes_created`, `mails_sent` and `social` fields are currently always zero. They are retained so this endpoint has the same `ActivityDay` response shape as profile activity. #### Visibility options The `profile/options` folder holds [boolean toggles](/docs/reference/api/files.md#value-spaces) (`yes`/`no`, default `no`), again founder/admin only: | Option | Effect | | ------------------ | ---------------------------------------------- | | `public` | Whether the group/org page is reachable at all | | `show_profile` | Show the profile page | | `show_description` | Show the description on it | | `show_members` | Show the member list | | `is_open` | Let users join without an invite | Organisations also have two nullable identity colours. These are small dashboard accents, separate from the organisation's full visualisation theme: | Verb | Path | Description | | ------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------ | | `GET` / `POST` / `DELETE` | `/admin/orgs/:org/profile/options/primary_color` | Primary identity colour, used for organisation identity and aggregate activity | | `GET` / `POST` / `DELETE` | `/admin/orgs/:org/profile/options/secondary_color` | Optional secondary accent | `POST` takes a plain-text CSS hex colour in strict `#RRGGBB` form: a leading `#` followed by exactly six hexadecimal digits. A trailing editor line ending is removed; other surrounding whitespace is invalid. Hexadecimal letters may be upper- or lowercase and are preserved. Malformed values return `400` without replacing the stored colour. The management leaves are available on premium and enterprise plans; reading or writing them requires an accepted organisation `founder` or `admin` role. `DELETE` idempotently resets the selected colour to `null` and returns an empty plain-text response. A `GET` on an unset colour returns an empty value; the organisation GraphQL fields and `user_info.primary_org` overview fields return `null`. When the primary colour is null, dashboard surfaces use the standard Novem colour. A null secondary colour means that no organisation-specific secondary accent is configured. > **Note:** for **organisations** and **org groups** the `show_*` options > require a **premium** subscription. User-group options are available on all > plans. Groups and orgs also carry comment [threads](/docs/reference/api/social.md) at `.../threads/*`, on the same paid-to-write, free-to-read terms as the rest of the platform. Org **themes** (the branding applied to a whole organisation's content) are documented separately under [org themes](/docs/reference/theme/org-themes.md). ### Browsing groups & orgs The management endpoints above live under `/admin`. The read-oriented views are public (subject to the visibility options): | Path | Description | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `/v1/orgs` | Organisations visible to you | | `/v1/orgs/:org` | An organisation — description and groups | | `/v1/orgs/:org/groups` | The org's groups — all of them for an org admin, otherwise those you belong to | | `/v1/orgs/:org/groups/:group` | A group — name, description, profile, `role` (your role in it, empty until you hold one) and `shared` content | | `/v1/orgs/:org/profile/stats/activity` | Daily top-level runs for jobs shared readably through your child org groups | | `/v1/users/:user/groups` | A user's groups visible to you | | `/v1/o/:org` | The org [FQNP](/docs/reference/api/terminology.md) alias — `card`, `banner`, `img`, and the org's visuals under `p` / `g` / `m` / `d` / `v` | ## Novem Markdown **Simple markdown** includes everything all features listed below and can be used everywhere you use text on the novem platform. This includes chart and table data, configurations, as well as captions, titles, descriptions and names. **Advanced markdown** extends simple markdown, adding more advanced features. Advanced markdown is only usable in document and e-mail content files. ### Simple markdown Simple markdown is intended to allow you access to rich formatting features for your text. #### Headings Novem support 5 levels of headings denoted by the hash character in front of the text. Markdown Result ```markdown # Heading 1 ## Heading 2 ### Heading 3 #### Heading 4 ##### Heading 5 ``` Heading 1 Heading 2 Heading 3 Heading 4 Heading 5 #### Quotes Novem also allows formatted and nested quotes using the `>` character. Markdown Result ```markdown > This is a quote > > > This is a double quote > > > > > This is a triple quote ``` > This is a quote > > > This is a double quote > > > > > This is a triple quote #### Lists Markdown lists can be created using \* to create unordered lists and 1. 2. etc to create ordered ones. Markdown Result ```markdown - item one - item two - sub item two - one - sub item two - two ``` - item one - item two - sub item two - one - sub item two - two ```markdown 1. item one 2. item two 3. item three ``` 1. item one 2. item two 3. item three #### Bold and italic Bold and italics are created with two and three \* respectively. Markdown Result ```markdown It's easy to add **strong**, _italic_ and **_strong italic_** emphasis with markdown. ``` It's easy to add **strong**, *italic* and ***strong italic*** emphasis with markdown. #### Super and sub script Markdown supports superscript and subscript formatting using single carets `^...^` and single tildes `~...~` respectively. Markdown Result ```markdown This is a ^superscript^ and a ~subscript~ ``` This is a superscript and a subscript ### Advanced Markdown With advanced markdown, novem is expanding on the more classic markdown syntax by adding blocks with special meaning. These blocks allow you to control additional aspects of your visualisation, for example by including plots in e-mails or pages in reports. #### Sections Novem markdown has a special multi line construct called a section. The section start with `{'{{'}` followed by a `type`. You can then provide a set of options in yaml format before closing the section with `{'}}'}`. ```text {'{{ section'} option name: value {'}}'} ``` Examples of this would be page breaks, callouts, visualisations or images. Some more complex sections allows both a start and closing section such as a callout, in this case the callout section is closed with a leading /. example: ```yaml {'{{ callout'} type: warn border: none {'}}'} Content of the callout body in **markdown** format {'{{ /callout }}'} ``` ## Sections *Available in: documents and e-mails.* ### Section format Sections build on the grammar introduced in the [novem markdown overview](/docs/reference/markdown/overview.md#advanced-markdown). There are two fundamental kinds: - Single statement sections - Multi statement sections #### Single statement sections The single statement section consists of a single statement and its parameters. ```yaml {{ section_type # section open and type param: one # parameters param: two # }} # section close ``` This is mostly used for things that embed other resources — visualisations, images, author blocks. #### Multi statement sections Multi statement sections consist of an opening statement followed by a closing statement later in the document. The closing statement is the same as the open statement but with a leading slash `/` and no parameters. ```yaml {{ section_type # section open and type param: one # parameters param: two # }} # closing the top, but opening the # the multi section Other text and markdown # section content supplied by the user. * list * item {{ /section_type }} # closing multi section tag ``` This is used for wrapping longer-form content — paragraphs, callouts, preview text. Nesting sections inside each other is not supported in e-mails. #### Simplified sections If a section needs no parameters (using the defaults), the open and close statements can be written on single lines: ```yaml {{ section_type }} content {{ /section_type }} ``` See the [preview section](/docs/reference/markdown/preview.md) for an example of this. #### Inline sections Single statement sections also support an inline syntax with comma-separated parameters on one line: ```yaml {{ vis ref: /u/demo/p/chart, width: 80%, align: left }} ``` ### Common parameters All novem sections support a few common parameters for spacing, borders and colours. #### Spacing controls Use generic `padding` (`p`) and `margin` (`m`) controls on any section. The syntax is similar to the [plot table padding](/docs/reference/plot/config/table/padding.md): a directional indicator and a magnitude between 0 and 5. ```novem-color-border l -- add left padding to section r -- add right padding to section t -- add top padding to section b -- add bottom padding to section x -- add left and rigth padding to section y -- add top and bottom padding to section a -- add paddings to sides of the section ``` To supply multiple values use yaml array notation. ##### Examples Add a magnitude 2 padding around a paragraph. ```yaml {{ paragraph padding: a2 }} Paragraph text {{ /paragraph }} ``` Add an inner padding of 3, a margin of 2 at the top and bottom along with a 1 wide top border of color `gray-500` for a paragraph. Also using short forms where possible. ```yaml {{ para p: a3 m: y2 b: t1 gray-500 }} Paragraph text {{ /para }} ``` Add a padding of 1 to the left and 4 to the right. ```yaml {{ para p: [l1, r4] }} Paragraph text {{ /para }} ``` #### Borders The border grammar is very similar to the spacing controls above: the parameters are `border`, `borders` or `b` along with a direction, size and optional color code. ##### Examples Add a border all around a paragraph, the `inverse` color will be used by default. ```yaml {{ para b: a1 }} Paragraph text {{ /para }} ``` Add a paragraph with red left border and green right border. ```yaml {{ para b: [l1 red, r1 green] }} Paragraph text {{ /para }} ``` #### Colors Foreground and background colors are supported on all sections via `foreground` (`fg`) and `background` (`bg`). Like all novem colors you can either specify a named novem color or an explicit hex code. If you want to specifically set a dark mode variant then two colors can be supplied instead. ##### Examples A paragraph with a light background with darker text and a solid outer border. ```yaml {{ para b: a1 inverse bg: gray-300 fg: gray-800 }} Paragraph text {{ /para }} ``` ## Paragraph *Available in: documents and e-mails. The positioning options (`align`, `width`, `height`) and the `xxxl` font size are document-only.* ```yaml {{ paragraph font size: l font style: b font align: center fg: blue-700 }} Styled markdown text {{ /paragraph }} ``` ### Overview The paragraph section is a wrapper that applies styling across several markdown elements. It supports font size, style and alignment in addition to the [common parameters](/docs/reference/markdown/sections.md#common-parameters) for spacing, borders and colours. In documents a paragraph can also behave as a sized, aligned block on the page through the positioning options below. ### Options #### Font Size ```text keyword: font size / sz options: xs | s | m | l | xl | xxl | xxxl (xxxl: documents only) ``` The size is applied to all markdown elements inside the paragraph, bullets and text alike. E-mails support sizes up to `xxl`. #### Font Style ```text keyword: font style / st options: s | strong | b | bold | i | italic | regular | r ``` Styles can be combined with a space, e.g. `b i` for bold italic. The `regular` / `r` reset is document-only. #### Font Align ```text keyword: font align / al options: l | left | r | right | c | center | j | justify ``` ### Document-only options The following options size and position the paragraph as a block on the page. They have no effect in e-mails. #### Align ```text keyword: align options: < | > | - | ^ | v (combine up to two, e.g. -^, >v) ``` Box-level alignment for the paragraph's content using the same single-character grammar as matrix tables. `<`, `-` and `>` are the horizontal axis (left, center, right); `^` and `v` are the vertical axis (top, bottom; middle is the default). Symbols can be combined, e.g. `-^` for centered horizontally and pinned to the top, `>v` for right-aligned at the bottom. Setting `align` switches the paragraph to a flex column so children can be positioned on both axes — vertical positioning only has a visible effect when the paragraph also has a height (see `height` below). The horizontal symbol additionally sets `text-align` so inline text inside child paragraphs follows the same direction. #### Width ```text keyword: width options: any CSS length, e.g. 80%, 240px, 30rem ``` Sets an explicit width on the paragraph box. The value is passed through to CSS verbatim, so anything `width:` accepts is valid. #### Height ```text keyword: height options: any CSS length, or 100% ``` Sets an explicit height on the paragraph box. Use `height: 100%` to opt the paragraph into the same leftover-space pass that fills `{{ vis }}` blocks marked with `height: 100%`: the layout engine measures every other element on the page first and assigns the flex paragraphs the remaining vertical space. This is what makes full-bleed callouts or hero blocks work on a fixed-orientation page. ### Examples #### Styled heading block ```yaml {{ para sz: xxl st: b al: center fg: blue-800 m: t4 b2 }} # Document Title {{ /para }} ``` #### Disclaimer text ```yaml {{ para sz: s st: i fg: gray-600 b: l2 gray-400 p: l2 }} This document is for informational purposes only and should not be used for investment decisions. {{ /para }} ``` #### Finding block with accent border ```yaml {{ para b: l3 blue-500 p: l2 m: b2 }} **Finding 1:** The flexicurity model is increasingly a Danish phenomenon rather than a Nordic one. {{ /para }} ``` #### Hero block filling the page (documents) ```yaml {{ para width: 100% height: 100% align: - sz: xxxl st: b fg: blue-800 }} A bold statement, centered on a page of its own. {{ /para }} ``` The `height: 100%` opts the paragraph into the leftover-space pass so it expands to fill whatever vertical room the rest of the page leaves unused; `align: -` centers the content on both axes within that box. ## Callouts *Available in: documents and e-mails.* ```yaml {{ callout # section type type: warn # type of callout desc: Warnings reported by our # callout prefix system # message border: true # callout border }} We let our users generate helpful # callout content can callouts to display in their e-mails. # be written using # simple novem markdown Currently supported are * Warnings * Errors * Information * And success {{ /callout }} # callouts must be terminated # with the /callout section ``` ### Overview Callouts provide a way to draw attention to important information. They come in four visual styles — info, warning, danger and success — and work the same way in documents and e-mails. A callout is a [multi statement section](/docs/reference/markdown/sections.md) with the keyword `callout`; its content supports simple novem markdown. ### Options Options to the section are supplied as yaml within the double brackets. #### Type ```text keyword: type options: info | warn | danger | success aliases: warning → warn, error / err / alert → danger, succ → success default: info ``` The type determines the visual style and color of the callout box. Use `info` for general information, `warn` for important notices, `danger` (or its synonym `error`) for critical alerts, and `success` for positive confirmations. The aliases are accepted everywhere, so `danger` and `error` render identically in both documents and e-mails. #### Border ```text keyword: border options: true | false default: false ``` Controls whether a visible border is rendered around the callout box. #### Desc ```text keyword: desc options: "Description text" ``` An optional short prefix or description that appears above the callout content in a smaller, muted style. ### Examples #### A simple notice ```yaml {{ callout type: info }} Please note that data for January 18th has been estimated. {{ /callout }} ``` #### Adding border and descriptions ```yaml {{ callout type: warn desc: System Notice border: true }} **Warning:** The following data sources are currently unavailable: * Market data feed * Currency exchange rates Expected resolution: 2024-01-15 09:00 UTC. {{ /callout }} ``` #### A danger callout ```yaml {{ callout type: danger border: true }} **Critical:** This report could not be generated. Please contact the analytics team for assistance. {{ /callout }} ``` ## Visualisation *Available in: documents and e-mails.* *Embedded visualisation:* `/u/novem_demo/p/state_pop` ```yaml {{ vis # section type ref: /u/novem_demo/p/state_pop # vis to embed width: 100% # width align: left # center include title: false # no title include caption: true # include existing caption include link: true # include link to read more }} ``` ### Overview Visualisation sections let you embed live novem plots and grids directly in your content. The section is a [single statement section](/docs/reference/markdown/sections.md) with the keyword `vis`; in documents the closing `{{ /vis }}` tag is optional. ### Options Options to the section is supplied as yaml within the double brackets. #### Reference ```text keyword: ref options: shortname | local path | user path | official Novem URL ``` To embed a visualisation the only mandatory attribute is a reference to which visualisation to add. All of these spellings use the same reference resolver: - `XVBzV` — bare shortname - `/p/sales` or `p/sales` — local path, with or without a leading slash - `/u/acme/p/sales` or `u/acme/p/sales` — explicit user path - `https://novem.io/u/acme/p/sales` — official Novem URL The type segment may be `p`, `g`, `m`, or `d` for a plot, grid, mail, or document. Long local spellings such as `/plots/sales` are accepted too. ##### Shortname The shortname is simply the shortname of the visualisation (available for all visualisations as a `shortname` endpoint), examples would be [`qNGgN`](https://novem.io/u/novem_demo/p/state_pop) or [`2v1rV`](https://novem.io/u/novem_demo/p/unemployment_nordics), referring to two of the charts on our front page. The public URL form `https://novem.io/p/` resolves to that same shortname. ##### Local Path Local paths resolve against the owner of the document or mail. Both compact forms (`/p/state_pop`, `p/state_pop`) and long forms (`/plots/state_pop`, `plots/state_pop`) are accepted. ##### Global Path User paths let you refer to another user's visualisation by username and name. This matches the existing API paths to view user visualisations under `/u///`. The leading slash is optional, and an official URL is equivalent. These three references therefore resolve identically: ```text /u/novem_demo/p/state_pop u/novem_demo/p/state_pop https://novem.io/u/novem_demo/p/state_pop ``` #### Alignment ```text keyword: align options: left | right | center default: center ``` Alignment refers to the position of the plot in the context of it's layout. If the plot width is not 100%, then alignment is used to position it. #### Width ```text keyword: width options: 1-100% default: 100% ``` Width refers to the width of the plot relative to the current layout. If you have a two column layout than a width of 100% will refer to the full width of a column. #### Include Title ```text keyword: include title options: true | false default: true ``` Include the visualisation title if the visualisation has a title value set. #### Include Caption ```text keyword: include caption options: true | false default: true ``` Includes the visualisation caption if the visualisation has one. #### Include Link ```text keyword: include link options: true | false default: false ``` This option controls whether a small button with a link to the visualisation web page should be included. #### Override Title ```text keyword: override title options: "New title string" ``` This option allows you to override the title for the plot, the string has simple markdown support. #### Override Caption ```text keyword: override caption options: "New caption string" ``` This option allows you to override the caption for the plot, the string has simple markdown support. ### Examples #### Embed a plot with full width ```yaml {{ vis ref: /u/novem_demo/p/nei_rgn_perf width: 100% include title: false include caption: true }} ``` #### Inline syntax ```yaml {{ vis ref: /u/novem_demo/p/nei_rgn_tb, width: 80%, align: left }} ``` #### Side by side on a landscape page (documents) ```yaml {{ page orient: landscape cols: 2 }} {{ vis ref: /u/novem_demo/p/nei_rgn_perf width: 100% }} {{ vis ref: /u/novem_demo/p/nei_rgn_hier width: 100% }} ``` ## Images *Available in: documents and e-mails.* ```yaml {{ img # section type (image also works) ref: logo.png # asset to embed alt: The novem logo # accessibility text width: 200 # bare numbers are pixels align: center # left | right | center caption: Fig 1 — the logo # optional figcaption link: https://novem.io # optional click-through }} ``` ### Overview Where the [vis section](/docs/reference/markdown/visualisations.md) embeds a live novem visualisation, the `img` section embeds a static **asset**: a file you've uploaded to a plot, document or mail under its `assets/` path (`/v1/users/:user/vis/{p,d,m}/:vde/assets/`). It is a [single statement section](/docs/reference/markdown/sections.md) — self-closing, no content — with the keyword `img` (`image` is accepted as an alias, for symmetry with `vis`/`visualisation`). In e-mails the asset is not hot-linked: at send time the image bytes are attached to the message itself and referenced inline, so the e-mail renders at full fidelity offline, in clients that block remote images, and across forwards. In terminal (ANSI) renders the image degrades to its alt text: `[image: …]`. ### Referencing an asset ```text keyword: ref options: local asset name | FQNP ``` The only required option. Two shapes are accepted: ```yaml {{ img ref: logo.png }} # asset on this document/mail {{ img ref: /u/alice/p/myplot/a/diagram.svg }} # another VDE's asset (FQNP) {{ image ref: /u/alice/p/myplot/assets/diagram.svg }} # FQNP long form ``` A bare name refers to an asset uploaded to the hosting document or mail itself; the FQNP forms pull an asset from any visualisation you can read. ### Options #### Alt ```text keyword: alt options: "accessibility text" default: the asset's file name ``` Alternative text for screen readers and clients that don't render images. Also the text shown in terminal renders. #### Width / Height ```text keyword: width / height options: CSS length — 200, 200px, 80%, 12em, auto ``` Sizes the image. Bare numbers are treated as pixels. Pixel values also render correctly in desktop Outlook, which ignores CSS sizing on images; percentages and other units apply everywhere else. #### Align ```text keyword: align options: left | right | center ``` Positions the image within its layout when it's narrower than full width. #### Caption ```text keyword: caption options: "caption text" ``` An optional caption rendered below the image. Plain text only — markdown is not processed here. #### Link ```text keyword: link options: URL ``` Wraps the image in a link. Accepts `http(s)`, `mailto` and relative or anchor targets; other schemes are rejected. ### Examples #### A sized, centered logo ```yaml {{ img ref: logo.png alt: Acme Research width: 180 align: center }} ``` #### A captioned figure from another visualisation ```yaml {{ img ref: /u/novem_demo/p/state_pop/a/methodology.png alt: Methodology flowchart width: 80% caption: Figure 2 — how the population estimates are derived }} ``` #### Inline syntax with a click-through ```yaml {{ img ref: banner.png, alt: Read the full report, link: https://novem.io/u/acme/d/q3_report }} ``` ## Preview *Available in: e-mails only.* ```yaml {{ preview }} Markdown formatted preview text {{ /preview }} ``` ### Overview The preview section allows you to specify the text that appears in your recipients' inbox list view — the short snippet visible before they open the e-mail. This text will not render in the e-mail body itself. Most e-mail clients display 40-140 characters of preview text depending on the device and client. We recommend keeping preview text concise and informative. ### Adding a preview A preview is added by creating a `{{ preview }}` section at the beginning of your e-mail content. The content between the opening and closing tags will be used as the preview text. ```yaml {{ preview }} Q4 performance summary: North America +5.1%, Europe +1.2%. {{ /preview }} # Quarterly Report The full e-mail body starts here... ``` The preview section should be placed before any other content in the e-mail. Only one preview section should be used per e-mail. ### Examples #### Simple preview ```yaml {{ preview }} Your daily analytics summary for January 15, 2024. {{ /preview }} ``` #### Preview with key metrics ```yaml {{ preview }} Revenue up 18% YoY. 3 regions outperforming benchmark. {{ /preview }} ``` ## Authors *Available in: documents and e-mails.* ```yaml {{ author # section type username: novem_demo include bio: true override bio: Novem Demo is the novem example account used to create illustrations of how the novem platform can be used. include picture: true }} ``` ### Overview The author section lets you include an author information block in your e-mail. This displays the author's name, profile picture and biography pulled from their novem profile. You can also override the biography text with a custom message. ### Including authors An author block is added by creating a `{{ author }}` section in the [content](/docs/reference/mail/content.md#content-file) file. The author section is a single statement section — it does not require a closing tag. ### Restrictions The author must have the `allow_as_author` permission set on their profile. In addition, the visualisation must be shared with the author before the author can be tagged. A public sharing is not sufficient — there must be a joint group membership. ### Options Options to the section are supplied as yaml within the double brackets. #### Username ```text keyword: username options: novem username ``` The novem username of the author to include. This is required. #### Include Name ```text keyword: include name options: true | false default: true ``` Controls whether the author's profile name is displayed. #### Include Biography ```text keyword: include bio options: true | false default: true ``` Controls whether the author's biography is displayed. If an override bio is supplied, this is automatically set to true. #### Override Biography ```text keyword: override bio options: "Custom biography text" ``` Replaces the author's profile biography with custom text. Supports simple markdown formatting. #### Include Picture ```text keyword: include picture options: true | false default: true ``` Controls whether the author's profile picture is displayed. ### Examples #### Adding an author ```text {{ author username: novem_demo }} ``` #### Writing a custom bio ```yaml {{ author username: novem_demo override bio: Senior analyst at Acme Corp with 10 years of experience in quantitative research. include picture: true }} ``` ## Pages *Available in: documents only.* ```yaml {{ page # page break orient: landscape # page orientation cols: 2 # number of columns column-fill: auto # fill columns sequentially (default: balance) type: fp # page type }} ``` ### Overview The `{{ page }}` section creates a new page in your document. Each page can have its own orientation, column layout and type. Content before the first `{{ page }}` becomes page 0. The page section is always self-closing — there is no `{{ /page }}` tag. ### Options #### Orientation ```text keyword: orient / orientation / o options: portrait / p / port | landscape / l / land default: portrait ``` Controls whether the page is portrait (tall) or landscape (wide). #### Columns ```text keyword: cols options: 1 | 2 | 3 default: 1 ``` Sets the number of columns for the page layout. Content flows across columns left to right. #### Column Fill ```text keyword: column-fill options: balance | auto default: balance ``` Controls how content is distributed across columns when `cols` is greater than 1. - `balance` — content is split evenly so all columns end at roughly the same height. This is the CSS default and matches what most print and editorial layouts expect. - `auto` — content fills the first column to the bottom of the page before flowing into the next. Useful for newspaper-style pages or when you want the second column reserved for overflow only. `column-fill` is ignored on single-column pages. #### Column Break Use `{{ colbreak }}` inside a multi-column page to force the following content into the next column. It is a self-closing token with no attributes: ```yaml {{ page cols: 2 column-fill: auto }} This text fills the first column. {{ colbreak }} This text starts at the top of the second column, regardless of how much space was left below the previous block. ``` `{{ colbreak }}` only takes effect on CSS multicolumn pages — pages with `cols > 1` and no `height: 100%` vis or paragraph child. When a flex-height child is present the page is rewrapped into manual column buckets and `{{ colbreak }}` is currently ignored; in that case, lay out the columns by ordering content explicitly or split into separate `{{ page }}`s. #### Type ```text keyword: type options: fp | index | centered | blank ``` Controls the page type: - `fp` — front page, typically used for cover pages - `index` — index or table of contents style - `centered` — centered content layout - `blank` — empty page #### Footer ```text keyword: footer options: true | false | "Custom footer text" default: true ``` Controls whether a footer is displayed and optionally sets custom footer text. #### Page Number ```text keyword: pnum options: "auto" | "custom string" ``` Overrides the page number display. #### Auto-pagination ```text keyword: autopaginate options: true | false default: true ``` When a page's content exceeds the available space, novem automatically flows the overflow onto continuation pages that inherit the original page's `orient`, `cols`, `column-fill`, `type`, and `footer` settings. **What carries over:** - Lists keep numbering across the split — an `
    ` that splits between items 3 and 4 starts the continuation list with `start="4"`. - Tables reprint their `` (or leading `` row) and `` on every continuation, so column widths and header labels stay aligned. - Callouts and blockquotes split with their chrome (padding, border, background) replicated on each page they cross. - Headings move forward when they'd otherwise be orphaned at the bottom of a page with no following content to keep them company. - Footnotes follow the reference. A `[[ftn: ...]]` whose `` ends up on the second continuation has its body rendered in that page's footer, not stranded on the original. - Page numbering resets at format transitions — a doc with `pnum: i` prologue pages followed by arabic body pages reads `i, ii, iii, 1, 2, 3`. **Opting out:** Set `autopaginate: false` to clip overflow as before — useful for cover, index, TOC, and `centered` / `blank` pages where you've already hand-tuned the layout. Those page types are also skipped automatically; you only need the explicit override for `content` pages. Set as document frontmatter to control the default, or on an individual page to override: ```yaml --- title: My Document autopaginate: false # disable across the whole doc --- {{ page autopaginate: true # but re-enable for this one }} ``` **Theme hooks:** Continuation pages are stamped with `data-auto-cont` (presence-only) and `data-auto-seq="N"` (1-based continuation index) on the page wrapper. Themes can target either: ```css /* Hide the running header on continuations */ .novem--doc--page-wrapper[data-auto-cont] .novem--doc--page-header { visibility: hidden; } /* Or just mark them */ .novem--doc--page-wrapper[data-auto-cont] .novem--doc--page-header::after { content: " (continued)"; color: var(--novem-text-muted); font-size: 0.85em; } ``` The `--doc-title`, `--doc-subtitle`, `--doc-author`, and `--doc-date` CSS custom properties are stripped on continuations, so themes that use `content: var(--doc-title)` in `::before` rules automatically suppress the title on continuations without any extra work. **`data-page-num` vs `data-page-index`:** - `data-page-num` (1-based) counts every rendered page including continuations. This is what readers see. - `data-page-index` (0-based) identifies the source `{{ page }}` — continuations of source page 3 all carry `data-page-index="3"`. Use this when styling "the second author-defined page" regardless of how many continuations it spans. **Caveats:** - `nth-child` selectors on page wrappers count continuations. Use `[data-page-num]` or `[data-page-index]` attribute selectors instead if you want to target the author's logical pages. - Multi-column with `column-fill: balance` and tightly packed tall atoms can rarely overflow by a few pixels — the engine balances columns post-pack, which can land one column slightly taller than the others. Use `column-fill: auto` if exact fit matters. - Paragraphs aren't split mid-line. A single paragraph taller than the page gets placed on its own page; if it's *still* taller than the page, it clips at the bottom. #### Reference ```text keyword: ref options: FQNP | shortname | local path | global path ``` Embeds another novem visual as a full-page element. Supports references to documents (`/d/`), plots (`/p/`) and grids (`/g/`). The embedded visual replaces the page content entirely. ### Examples #### A simple multi-page document ```yaml # Cover Page {{ page orient: portrait }} # Chapter One Content for chapter one. {{ page orient: landscape cols: 2 }} ## Data Overview Two-column landscape layout for charts. ``` #### Embedding another document as a page ```text {{ page ref: /u/novem_demo/d/quarterly-report }} ``` #### Embedding a grid dashboard ```yaml {{ page orient: landscape ref: /u/novem_demo/g/nei_rgn_dash }} ``` #### Mixed layouts ```yaml --- title: Quarterly Review --- # Q4 2024 Review {{ page orient: portrait type: fp }} ## Executive Summary {{ page orient: landscape cols: 2 }} ## Charts and Data {{ page orient: portrait type: centered }} ## Conclusions ``` ## Page references *Available in: documents only.* ```markdown Our runway is healthy (more detail on page {pageref##Financial performance and runway}). ``` ### Overview A page reference inserts the **page number** of a heading somewhere else in the document. You write which heading you mean; novem fills in the number. The number is resolved **after** the document has been laid out — once pages have been split and renumbered — so it is always the real, final page the heading lands on, even when content reflows or auto-paginates onto extra pages. ### Syntax ```text {pageref##Heading text} ``` - It starts with `pageref`. - The run of `#`s is the **heading level** you're targeting: `#` matches an `h1`, `##` an `h2`, `###` an `h3`, and so on. - The rest is the **heading text** to match. #### Matching The target is matched against the headings of that level, comparing them **normalised**: whitespace is removed, trailing `{instructions}` (like `{colspan:all}`) are stripped, and case is ignored. So all of these match the heading `## Financial performance and runway {colspan:all}`: ```markdown {pageref##Financial performance and runway} {pageref##financial PERFORMANCE and Runway} ``` If more than one heading matches, the **first** one (in document order) wins. If nothing matches, the reference renders as `#N/A` so it's easy to spot. ### Forms #### Plain page number The page number as plain text: ```markdown See page {pageref##Capital allocation} for the breakdown. ``` #### Clickable Wrap the heading text in parentheses to make the page number a **link** that jumps to the heading: ```markdown See page {pageref##(Capital allocation)} for the breakdown. ``` #### Aliased link Give it your own label with a markdown-style `[label](target)` — the label becomes the clickable text (linking to the heading) instead of the number: ```markdown The full breakdown is {pageref##[on the capital page](Capital allocation)}. ``` ### Examples ```markdown ## Summary Finally, at our current costs and revenue levels we have a healthy runway of at least 18 months (more details on page {pageref##Financial performance and runway}). {{ page }} ## Financial performance and runway ... ``` ```markdown For our strategy see {pageref##[Strategy & outlook](Strategy and outlook)}; capital allocation is covered on page {pageref###(Capital allocation)}. ``` ## Spans *Available in: documents only.* ```markdown Text with a footnote [[ftn: Additional context here]]. A citation [[ref: Smith 2024, Journal of Data Science]]. Status: [[clr,fg,green-600: OPERATIONAL]]. ``` ### Overview Span sections are inline elements that decorate or annotate text within a paragraph. They use the `[[ ]]` syntax and support three types: footnotes, references and colour spans. Span sections can contain nested inline formatting — bold, italic, underline and other markdown within the span content. ### Syntax ```text [[type,option1,option2: content]] ``` The span type comes first, followed by optional comma-separated parameters, then a colon and the content. ### Types #### Footnotes (ftn) Footnotes provide additional context without cluttering the main text. They are automatically numbered and collected at the bottom of the page or document. ```markdown The Nordic model combines flexibility with security [[ftn: The term flexicurity was coined in Denmark in the 1990s.]]. ``` #### References (ref) References are used for citations and are automatically collected into a bibliography or reference section. ```markdown As documented in [[ref: OECD Employment Outlook 2024]], Nordic countries spend significantly more on labour market policies. ``` #### Colour (clr) Colour spans apply inline foreground or background colours to text. Options follow the span type as comma-separated values. ```markdown Status: [[clr,fg,green-600: OPERATIONAL]] Alert: [[clr,fg,red-600: CRITICAL]] Note: [[clr,fg,orange-500: DEGRADED]] ``` ### Nested formatting Span content supports standard inline markdown: ```markdown [[ftn: This note contains **bold** and _underlined_ text.]] [[clr,fg,blue-600: This is **bold blue** text.]] ``` ### Examples #### Academic-style citations ```markdown This finding is consistent with earlier work by [[ref: Andersen (2015), The Danish Flexicurity Model]] and subsequent analysis by [[ref: Calmfors (2004), The Nordic Model of Labour Market Policy]]. ``` #### Status indicators ```markdown | Service | Status | | -------- | ------------------------------- | | API | [[clr,fg,green-600: Online]] | | Database | [[clr,fg,orange-500: Degraded]] | | Search | [[clr,fg,red-600: Offline]] | ``` #### Multiple footnotes ```markdown GDP figures are nominal [[ftn: All values in USD at current exchange rates.]] unless stated otherwise [[ftn: PPP-adjusted figures available in the appendix.]]. ``` ## Variables *Available in: documents only.* ```markdown Revenue was {/u/acme/p/revenue/v/total} million. ``` ### Overview Variables let you embed live values from any novem visualisation directly in your document text. When the underlying data changes, the variable value updates automatically across all outputs. Variables use the Fully Qualified Novem Path (FQNP) syntax wrapped in single curly braces. They render inline — within paragraphs, lists, callouts and other content. ### Syntax The variable syntax follows the pattern: ```text {/u////v/} ``` Where: - `` — the novem username who owns the visualisation - `` — the vis type: `p` (plot), `g` (grid), `m` (mail), `d` (doc), `r` (repo), `j` (job) - `` — the visualisation identifier - `` — the variable name defined on the visualisation ### Where variables work Variables can be used anywhere inline text is supported: - Regular paragraphs - Inside `{{ para }}` sections - Inside `{{ callout }}` sections - Inside list items - Inside `[[span: sections]]` Variables are **not** processed inside code blocks or inline code. ### Examples #### Inline metric ```markdown The index returned {/u/acme/p/nei/v/ytd_return} year to date. ``` #### Multiple variables in a paragraph ```markdown Revenue hit {/u/acme/p/rev/v/total} this quarter, up from {/u/acme/p/rev/v/prev} last quarter — a {/u/acme/p/rev/v/growth} increase. ``` #### Inside a styled paragraph ```yaml {{ para b: l3 blue-500 p: l2 }} **Current value:** {/u/acme/p/dashboard/v/current_price} {{ /para }} ``` #### Inside a list ```markdown - North America: {/u/acme/p/regions/v/na_return} - Europe: {/u/acme/p/regions/v/eu_return} - Asia Pacific: {/u/acme/p/regions/v/apac_return} ``` ## HTML *Available in: documents and e-mails.* ```yaml {{ html }}
    Custom HTML content
    {{ /html }} ``` ### Overview The HTML section passes raw HTML through to the renderer. This is useful for content that can't be expressed in standard markdown, such as complex tables, custom layouts or embedded widgets. Use HTML sections sparingly — markdown is preferred for consistency across output formats (web, PDF, e-mail). ### Including HTML The content between `{{ html }}` and `{{ /html }}` is stored as raw text and passed directly to the rendering layer. No markdown processing is applied to the content. ### Examples #### Custom styled table ```yaml {{ html }}
    Region Return
    North America +5.1%
    Europe +1.2%
    {{ /html }} ``` ## Comments *Available in: documents only.* ```markdown Visible text. {{/* This whole block is commented out — prose, sections and page breaks inside it are all ignored and never rendered. ## Even headings {{ vis ref: /u/me/p/draft }} */}} More visible text. ``` ### Overview A comment block hides everything between its markers from the rendered document. Use it to park a draft section, leave yourself a note, or pull something out temporarily without losing it. ### Syntax The opener `{{/*` and the closer `*/}}` each go **on their own line**: ```markdown {{/* ...anything here is hidden... */}} ``` Anything between them — paragraphs, headings, `{{ }}` sections, page breaks, lists — is removed before the document is parsed, so it has no effect at all on the output (it won't even create a blank page). An inline `{{/*` that shares a line with other text is **not** a comment — the markers only count when alone on a line. ### Rules #### No nesting Comments do not nest. The **first** `*/}}` closes the block; a `{{/*` that appears inside a comment is just part of the hidden content. ```markdown {{/* hidden {{/* still hidden — not a new comment */}} this line is visible again ``` #### Code fences win Comment markers inside a fenced code block (` ``` ` or `~~~`) are shown verbatim as code — they don't start or end a comment: ````markdown ``` {{/* this is sample code, not a comment */}} ``` ```` (A comment that *wraps* a code fence — opener before the fence, closer after — still hides the fence along with everything else, since the opener wasn't inside it.) ### Examples ```markdown ## Q2 results Revenue grew 18%. {{/* TODO: add the segment breakdown chart once the data is final {{ vis ref: /u/novem/p/segment_breakdown }} */}} Operating margin widened to 22%. ``` ## Overview A plot is a folder of plain files under `/v1/vis/plots/:plot`: you write `data` and the files under `config/`, and novem renders the result. This section is the reference for those files. [Config](/docs/reference/plot/config/overview.md) documents the option tree (type, colors, axes and friends); [Charts](/docs/reference/plot/charts/overview.md) documents each chart type and what it expects of your data; [Tables](/docs/reference/plot/tables/overview.md) covers the table-shaped plots; and [Vars](/docs/reference/plot/vars/overview.md) covers the small formatted data points (KPIs, summary statistics) you can attach to a plot. If you are new to plots, start with the [quick start](/docs/start/plot.md) or the [plot guide](/docs/guides/plot.md) instead; this section assumes you know what you are looking for. ### In this section ## Config By default your plot will follow the standard novem style, but that might not fit every situation. To remidy that novem offers an extensive set of options to control your visualisation. This section explains the available options as well as how to interact with it through our API. If you're using one of our [libraries](/docs/guides/libraries.md) then there might be some helper functions to make these changes. ### Structure As with all other novem interactions, the plot configuration can be modelled as a tree structure of different files, or endpoints, you can modify. This is done by writing a `HTTP POST` request with the content to the correct `URI`. Below is an overview of the available options along with their path and a short description. For more information bout the individual options check out their respective documentation pages. ```bash plot_name => Name ... ├── config => Configuration options │ ├── axis => The axis grammar (one file) │ ├── margins => Surface margins + panel gutter │ ├── facets => Panel carving (small multiples) │ ├── labels => Per-datapoint annotations (one file) │ ├── marks => Mark styling (one file) │ ├── colors => The ix color instructions (one file) │ ├── table => table specific settings │ │ └── cell => Format table cells │ │ ├── align => number alignment │ │ ├── border => control table borders │ │ ├── text => font information (weight, style, color) │ │ ├── format => number and text format │ │ ├── padding => cell padding │ │ ├── width => cell width │ │ └── merge => merge cells across cols and rows │ ├── legend => │ │ ├── type => The kind of legend │ │ ├── position => Position of the legend │ │ ├── layout => Layout, rows, grids, alignment │ │ ├── margin => Legend-scoped margins (the m/p pair) │ │ └── format => Format of the individual legend item │ ├── interactive => │ │ ├── format => │ │ └── highlight => │ ├── render => │ │ ├── template => Template to render against │ │ ├── orientation => Landscape or Portrait orientation │ │ └── scale => │ ├── projection => Map projection │ ├── zoom => Named geographic extent │ ├── position => Map focus, anchor, bearing, and relative scale │ ├── caption => Caption below chart │ ├── theme => │ ├── title => Title + optional subtitle (two lines) │ └── type => bar, sbar, gbar etc... ... ``` ## Type The `type` is probably the most important option of your novem plot. In addition to controlling what other options you can set, the type also controls what kind of plot you want to render. As all novem plots are live, the instant you change the type, the plot will update. If applicable, this will also trigger a render transition. ### Structure ```bash plot_name => Name ... ├── config => Configuration options ... │ └── type => bar, sbar, gbar etc... ... ``` ### Key novem types You'll notice that the documentation for the novem plot is split into sections such as [Charts](/docs/reference/plot/charts/overview.md) and [Tables](/docs/reference/plot/tables/overview.md). These sections and their visuals might look different from each other, but they are all just novem plots. So if you wish, you can update the type of your plot to go from a barchart to a table or a calendar. The reason for segmenting plots into the different category is a combination of logical grouping, shared configuration option or shared purpose. #### Charts The most common type of novem plots you'll make is probably charts. Chart contains all the most common visuals such as bars, lines, areas, maps, pies, scatters etc. An extensive effort has been made show helpful transitions when switching between plots in the chart category. #### Tables Tables have been placed in their own category as they share a lot of common options and formats. The [novem slicing](/docs/reference/api/slicing.md) functionality is primarily developed for styling tables. ### Sliced types The type file accepts more than a single scalar. Using the [novem slicing notation](/docs/reference/api/slicing.md) each line can claim a region of your data (rows × columns) for a mark type, so one plot mixes marks per cell — the first two series as a stacked bar with the last series as a line, or a series that switches from area to line where the forecast begins. ```novem-type-ix : 1:2 sbar -- all rows, the first two series: one stack : -1 line -- the last series as a line over it ``` Each line is whitespace-separated into one to three fields, with `--` starting a comment: ```text [row-slicer] [col-slicer] type ``` - **One token** is a bare line: each bare line claims the next unclaimed column, and the last bare line covers every cell nothing else matched. A file with exactly one bare token is the scalar form above. - **Two tokens** read as `col-slicer type` — a single slicer addresses columns (series). - **Three tokens** read as `row-slicer col-slicer type`. Row slicers partition the index, so the same series can change type along the chart (actuals as area, forecast rows as line). Each line paints its selection as an independent layer; two layers can select the same cells and both paint (a bar under a line). Invalid lines are logged and skipped, and the rest of the file survives. #### Line markers The shared `+` and `-` [line markers](/docs/reference/api/slicing.md) apply: - `+ ` attaches a layer to the nearest preceding plain line and follows its base's rules: a stacked variant accumulates in the base's stacking context, a bar variant shares the base's band slot. - `- ` erases the selected cells from every layer above it, and later lines still paint into the hole. Eraser lines take a slicer only, no type. ```novem-type-ix : : sbar -- everything one stack... - : 3 -- ...except the third series... : 3 line -- ...which draws as a line ``` #### The combo set Only the vertical cartesian family may appear on a sliced line: `line`, `sline`, `area`, `sarea`, `bar`, `gbar`, `sbar` and `scatter`, plus two special members: - `pie` — as an instruction its column slicer picks the series to pie (`: 2 pie`); the rows are the slices and the pie still owns the whole plot. - `none` — claims its cells like any other layer but draws nothing: the cells still count for extents, scales, facets and legends. `mline` and `marea` are deprecated aliases of `line` and `area` (which always drew multiple series); they are accepted anywhere a type is and normalize to their canonical family. Every other type — tables, maps, the horizontal `h*` family, data-reshaping types such as `hist`, `hexbin`, `bubble` and `aggbar`, the calendars (`cal`/`calendar` and their suffix variants) and `color` — is scalar-only: legal as the single type of the whole plot, never on a sliced line. ## Title and subtitle `plot` `config/title` The first line of `config/title` is the title. Add a second line for a subtitle. Both are placed at the top left by default. ```text Quarterly revenue Unaudited · Q2 2026 ``` The title is part of the chart SVG. Its theme defaults are equivalent to `ltx:500,18 lmb:2`; the subtitle defaults to `ltx:400,12 lmb:4`. That gives the two lines a tight, explicit 2px gap and leaves 4px before the plot body, with no hidden HTML padding or line-height. Plot-level `config/margin` sits outside the title as part of the chart's outer frame. It does not add space between the title and the marks. Use title-local `lmb:` when you explicitly want a gap below a title line. Each line can use the shared `l*` label controls: ```text Quarterly revenue ltx:b,18 le:r lc:#123456,#abcdef Unaudited · Q2 2026 ltx:12 le:r lmb:3 ``` `le:` accepts `l`, `c`, or `r` (and `left`, `center`, or `right`). `ltx:` accepts the same sizes, weights, style letters, and font families as other novem text surfaces. For example, `ltx:b,18` is bold 18px text. Use `lc:` to control the text color. A two-color value supplies light and dark mode colors; `lc:ink` restores the theme title or subtitle color: ```text Quarterly revenue lc:#123456,#abcdef Unaudited · Q2 2026 lc:ink ``` The other title-applicable label controls are `lb:` for a plate, `lr:` for rotation, and `lml:`, `lmr:`, `lmt:`, `lmb:` for explicit spacing. `lmx:`, `lmy:`, and `lma:` set two or all four sides. Left, right, and top spacing default to zero; bottom spacing uses the tight theme defaults above. An authored margin replaces its line's theme default. The shorter `tx:` and `e:` aliases remain valid on title lines. Use the explicit `l:` form when the title text itself looks like a directive: ```text l:"Use ltx:18 in a title" l:"A centered subtitle" le:c ``` The `%n` token inserts the plot name: ```text %n -- name from the plot's about metadata l:"Updated automatically" le:r ``` As in the other plot grammars, `--` starts a comment outside quotes. Only the first two non-trailing physical lines render. A third non-empty line is ignored and reported as a configuration diagnostic. ## Colors Plot colors live in a single configuration file: `/config/colors`. Its content is a set of index color instructions (the novem `ix` grammar), a line-based format that selects part of your data and assigns colors to it. In addition novem supports theming, where you can select from several pre-made themes or define your own (for professional or above subscribers) via the `/config/theme` endpoint. The last thing to keep in mind is that novem also supports [dark-mode](/docs/reference/theme/colors.md#what-is-dark-mode), so all themes and colors can have a corresponding dark-mode variant. ### Structure As can be seen from the [config overview](/docs/reference/plot/config/overview.md), the color configuration is one file alongside the other single-file grammars (type, axis, margins, facets). ```nohighlight en_letter_frequency => Name ... ├── config => Configuration options ... │ ├── colors => The color instructions (ix grammar) │ ├── theme => Color theme to use ... ├── data => data to chart ... ``` Like most novem API end-points the above values can be modified with a `POST` request containing the desired values. #### Theme The `theme` endpoint allows you to refer to a global, organisational or personal theme. Custom theme support is available for `professional` and above subscriptions. Everyone has access to the default theme `novem` along with `novem-light` and `novem-dark`. #### Deprecated: the colors folder `/config/colors` used to be a folder holding two files: a grammar selector (`type`) and the instructions (`colors`). The ix grammar is the only grammar, so the selector is gone and the instructions moved up to `/config/colors` itself. The old paths keep answering for existing integrations: - Writes to `/config/colors/colors` land transparently on `/config/colors`, and reads return the same content. - Writes to `/config/colors/type` are accepted and discarded; reads always answer `ix`. New integrations should use `/config/colors` directly. ### Index Colors If you want to add colors to any of your novem tables then you'll be using index colors: write your instructions to the `/config/colors` endpoint. Index colors use the [novem slicing notation](/docs/reference/api/slicing.md) to describe an area of the table and then the color instructions to color the selected segment. Index colors consists of three core components 1. Selectors - the row and column selectors 2. Plane - foreground or background 3. Color Instruction - the colors and associated information : : Selector A novem selector which denotes which rows and columns the color should be applied to. bg Plane instruction There are two dimensions or planes you can color in a table, foreground and background. blue-200 Novem Color Instruction Color `plane` of the `slice` given by the row and column selectors. See [below](#dynamic-colors) for an example of a complex color instruction. #### Static colors A single novem color makes a static instruction: the color is applied, unchanged, to the part of the table defined by the given selector. Both light and darkmode colors can be supplied explicitly; when the dark color is omitted it is inferred from the theme. ```text bg|fg color-light [color-dark] ``` ##### Creating a pattern This example shows a static background color pattern reminiscent of a picnic blanket using three shades of blue. *Embedded visualisation:* `/u/novem_demo/p/color_config_ix_picnic` Here we use four selectors and take particular advantage of the step instructor. First we color alternating rows starting at different offsets. Then we add alternating columns, overwriting select colors. ```novem-color-ix -- Create a "picnic blanket" using 4 shades of blue ::2 : bg blue-100 -- color every other row starting at 0 1::2 : bg blue-200 -- color every other row starting at 1 1::2 1::2 bg blue-300 -- color every other col starting at row 1 col 1 ::2 1::2 bg blue-200 -- color every other row starting at row 0 col 1 ``` ##### Adding a hierarchy Here we emulate a hierarchical structure in our table by coloring rows in descending shades of gray, a common strategy when wanting to preserve space. *Embedded visualisation:* `/u/novem_demo/p/color_config_ix_hierarchy` Unlike in the picnic example above, here we use a comma separated list of rows to shade, this is because there is no geometric pattern to the hierarchy, rather it depends on the data itself. ```novem-color-ix -- Color rows by "hierarchy" 1 : bg gray-500 -- world 2,5,8 : bg gray-400 -- region 1 3,6,9,12,19 : bg gray-300 -- region 2 ``` ##### Playing with squares Later instructions will override the colors of the previous instructions, below is an example drawing several "squares" in different colors. *Embedded visualisation:* `/u/novem_demo/p/color_config_ix_squares` As you can see the purple square is drawn on top of the blue and red squares, this is because colors are applied in the order they are defined. ```novem-color-ix -- Create some colorful "squares" 2:4 1:3 bg blue-200 6:8 1:3 bg red-200 10:12 1:3 bg green-200 4:6 3:5 bg purple-200 2:5 7:9 bg pink-200 8:11 5:7 bg yellow-200 -- Creating our "gradient" 3 11:-1 bg orange-100 4 11:-1 bg orange-200 5 11:-1 bg orange-300 6 11:-1 bg orange-400 7 11:-1 bg orange-500 8 11:-1 bg orange-600 9 11:-1 bg orange-700 10 11:-1 bg orange-800 11 11:-1 bg orange-900 ``` Changing the instructions above from `bg` to `fg` will color the text located in the cells instead of the background as can be seen below (with 400 instead of 200 colors along with a global gray-300). *Embedded visualisation:* `/u/novem_demo/p/color_config_ix_squares_fg` #### Dynamic colors So far we've only shown static colors, but novem also supports colors based on the value contained in the cells, we call this dynamic colors. Dynamic colors are created by providing a novem color instruction consisting of a color `range` along with optional `domain` and `scale`. A color range is a comma separated list of novem colors which is linearly interpolated over the corresponding cell values. To spell the scale explicitly, add a caret `^` followed by the scale to the end of the color instruction, e.g. `^lin`. Tables support linear interpolation only; [charts](#coloring-charts) additionally accept `^log` and `^exp`. Finally you can also control the domain of the underlying values, by default the values in the cells selected by the row and column selectors are used, but this can be overridden by supplying a `domain` function. The domain function consists of sequence of comma separated numbers contained in a bracket. The numbers will be matched to the corresponding color in the order supplied. If you want to mix and match values from the underlying dataset along with specific domain values, such as forcing 0 for a neutral color in a diverging dataset, you can use the `_` (underscore) operator. The `_` gets replaced with the corresponding value from the dataset based on its position in the domain. Below you can see an example of a Novem Color Instruction that creates a linear heatmap from red -> gray -> green with red for numbers below zero and green for numbers above. Numbers in the zero range are colored gray. **Note:** Please be aware that Novem Color Instructions should contain no space! bad,neutral,good range Create a color range of three novem theme colors. Here we use the subjective bad, neutral and good colors which are mapped to red, gray and green. (*,0,*) domain If we didn't specify a domain, the linear midpoint in our data could be located below or above zero. Here we use the domain function to force neutral to be zero whilst using the underlying value for the lowest (bad) and highest (good) colors. ^lin scale Finally we state that this should be a linear color interpretation. This is the default behavior, so technically this instruction could be skipped. ##### Creating a heatmap Here we use the above information to create a heatmap over a given dataset. *Embedded visualisation:* `/u/novem_demo/p/color_config_ix_dynamic_heatmap` As you can see we get a nice linear spread across our predefined color range. In addition we've included a separate selection in the center showing that those colors are interpolated within the designated value selection. ```novem-color-ix : : bg bad,neutral,good(_,0,_)^lin 4:9 4:9 bg bad,neutral,good(_,0,_)^lin ``` ##### A real world example Whilst the above example can look a bit stylized, below is a table using real world data from our Novem Example Index dataset. If you're curious about how we made this visual check out [our blog](/blog/creating-the-novem-example-index/). *Embedded visualisation:* `/u/novem_demo/p/nei_sector_perf` ### Coloring charts Everything above applies to tables and charts alike — the same selector, plane, and color grammar. Charts add two abilities on top: coloring by a column's values, and a few chart-specific planes and targets. Both are covered below. #### Coloring by category Often you don't want to color a fixed selection — you want each *value* of a column to get its own color. Colour a scatter of measurements by species, a bar chart of sales by region: one color per distinct value, drawn from an ordinal palette. That is the `by` instruction. ```text by ``` `by` reads like the rest of the grammar — a leading keyword, then the [selector](/docs/reference/api/slicing.md) naming the column whose distinct values drive the color, then the palette. Because a single slicer addresses a column, `by species` and `by : species` are the same instruction. by Value-color keyword Marks the line as data-driven: color by the values in a column rather than a fixed row/column selection. species Key column A selector naming the column whose distinct values become the color groups. The values are taken in first-appearance order. tab10 Palette A named ordinal palette (`tab10`, `tab20`, `tab20b`, `tab20c`) or an inline comma list of colors. Each distinct value takes the palette entry at its index, cycling if there are more values than colors. The three ways to supply colors: ```novem-color-ix -- Automatic: sample an ordinal palette across the distinct values by species tab10 -- Automatic from an inline palette by species blue,green,orange -- Explicit: pin specific values, the rest fall to the palette by species tab10 Gentoo:red -- The x column is a first-class key (the col-0 rule): every bar -- takes its own category's color by 0 tab10 ``` Column 0 — the x column — is addressable like any other key, by index (`by 0`) or by its header name: each mark takes the color of its row's x value, the each-bar-its-own-color spelling. The synthesized legend captions the categories. Explicit `value:color` pairs pin a color to a named value; any value you don't pin is filled from the palette (or the theme's categorical colors when no palette is given). Both light and dark variants are carried by named palettes automatically — and by theme tokens (`blue-500`) in inline palettes and pins, so dark mode comes for free unless you write raw hex. `by` also gives you the key to read it with: when your plot has no legend configured, the distinct values and their swatches render as an automatically placed legend. A legend you configure yourself always wins. `by` composes with facets, including on the facet key itself. Every row in a panel shares the key value, so the whole panel takes that value's color — the marks by default, or the panel background with the `bg` plane: ```novem-color-ix -- with facets: x : sex / y : species by : sex tab10 -- marks colored by the sex facet by : sex bg tab10 -- panel backgrounds tinted by sex by : sex bg red,green(_,_) -- panels tinted by their group average ``` The last form makes a heatmap of the facet grid: each panel's background samples the gradient at its group's average value, on a domain shared across all panels. #### Dynamic magnitude within a group A gradient after `by` colors each mark by its magnitude, grouped by the key column — the color scale runs across the values, the grouping decides the domain: ```novem-color-ix -- one shared color scale across all groups (comparable) by species red,blue^lin -- each group scaled to its own range (self-scaled small multiples) by species red,blue^lin ax:0 ``` A numeric key column is the magnitude itself: `by bill_length red,blue^lin` colors every mark by its bill length, over that column's own range. (The `^scale` — or an explicit `(domain)` — is what makes a color list a gradient; without it `red,blue` is a discrete palette cycling over the column's distinct values.) The `ax:` modifier is the same one facets use for sharing scales: `ax:xy` (the default) takes the magnitude range across *all* the data, so colors are comparable between groups and read against one legend; `ax:0` gives each group its own min-to-max. #### Planes Where a table cell has a foreground and a background, a chart mark has a few planes. Name one between the selector and the color: ```text [selector] [plane] color ``` fill The mark body — a bar's fill, a point's disc, a line's stroke. This is the default when no plane is given. stroke The outline — a point or bar border. text Data-label color. ```novem-color-ix : 1 stroke black -- the first series: a black outline : 1 fill blue-500 -- the first series: a blue body ``` Marks whose ink is a stroke — scatter points, line strokes — take `stroke`-plane colors as their ink directly; on a bar the `stroke` plane draws an outline around the body. A plane a plot doesn't draw is silently ignored. The table planes `bg` and `fg` still parse on a chart (they mean `fill`), so a color string authored for a table pastes onto a chart unchanged. #### Background A leading `bg` colors the plot's background surface instead of any data — light and dark: ```novem-color-ix bg gray-50 slate-900 ``` This is the per-plot override of the theme's surface color. On a faceted plot the lone form washes the **whole** surface — panels and the gaps between them alike. To tint individual facet cells instead, lead with the slicer pair over the facet grid — the same cell addressing the margins file uses: one slicer is columns, two are rows × columns, negatives count from the end. The older `r:`/`c:` key spelling keeps working: ```novem-color-ix bg 0 : blue-100 slate-900 -- the top facet row bg -1 1 red-100 red-900 -- one cell: last row, second column bg red-100 red-900 r:-1 c:1 -- the same cell, the older spelling ``` #### Palette cycling A line of exactly three dots hands the **unassigned** series to the theme palette, cycling from its first color: ```novem-color-ix 0 red-500 -- the first series is explicit ... -- everything else cycles the theme palette from c1 ``` Without the sentinel, unassigned series keep their theme default (the palette color of their own series index). The sentinel stands alone on its line and is position-independent. `bgi` is the inner sibling: it washes the area **inside** the plot frame — the region an axis `o` instruction outlines — leaving the margins and axis furniture on the page surface. On a faceted plot each panel washes its own inner box, and `r:`/`c:` scope it to specific cells the same way: ```novem-color-ix bgi blue-100 slate-800 -- every inner box bgi amber-100 amber-900 c:0 -- the first column's inner boxes only ``` Panel tints layer sensibly: a cell's `bg` paints the whole cell, its `bgi` paints only the frame's interior on top of it, and the marks draw above both. The `by … bg` form (color-by-facet, above) remains the value-driven way to tint panels. ## Table The table folder consists of several endpoints related to the formatting of your novem `table` or `mtable` visualisations. ### Structure ```bash plot_name => Name ├── config => Configuration options ... │ ├── table => table specific settings │ │ └── cell => Format table cells │ │ ├── border => control table borders │ │ ├── text => Font information (weight, style, color) │ │ ├── align => Number alignment │ │ ├── format => Number and text format │ │ ├── padding => Intra cell padding │ │ ├── width => Fixed column width │ │ ├── overflow => Text overflow control │ │ ├── priority => Responsive design control │ │ ├── merge => Set cell min/max width │ │ └── chart => Horizontal bar charts in cells ... ``` #### Table options #### Cell options The cell folder contains a collection of files which controls the formatting of the individual cells in the table. Each file consists of a set of instructions, separated by newline, applied in order from first to last. All instructions share a common structure consisting of space delimited options. Across all files the first two instructions are used to identify the rows and columns to be operated on. For more information see [slicing](/docs/reference/api/slicing.md "novem slicing syntax"). ## Border Novem tables offer extensive control over border formatting. Similar to other table formatting instructions the border format consist of row and table slicers followed by position, size and color. The slicing operations and colors are the same as for other novem table options, however the position and size instructions are unique to the border. Size are simply a value between 1 and 5 denoting the thickness of the border, position is slightly more complex and encodes the position of the border relative to the cell. The available position options are as follows: ```novem-color-border l -- add left border to cell r -- add right border to cell t -- add top border to cell b -- add bottom border to cell x -- add left and right border to cell y -- add top and bottom border to cell a -- add all borders to cell ``` We've included a few examples on how to use the border options below, feel free to follow the links and use the dropdown for more details on how the examples were created. **E-Mail table warning:** Sometimes you might notice that certain cell borders are being prioritized over the ones you prefer (like a background grid taking precedence over a line). Unfortunately this is the result of how html tables work. The easiest way to avoid this is by being more explicit in your instructions to avoid drawing multiple borders next to each other. It's also helpful to know that larger borders are prioritized over smaller, so if feasible, you can always increase the size. For more information read about [border conflict resolution](https://www.w3.org/TR/CSS2/tables.html#border-conflict-resolution) over at the w3c. #### Basic border The below table shows one of the simplest border you can have, a plain grid with a soft shade of gray. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_border_grid` The `a` operator adds borders to all sides of the cell and the two `slicers` select the entire table. `ctrl` `/config/table/cell/border` ```novem-color-border : : a 1 gray-400 -- add a gray grid of size 1 to all sides ``` #### Top and bottom This example shows how to add a border to the top and bottom row of a table, this can look particularly nice when combined with bold text or custom color styles. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_border_location` Here we use the `slicers` to explicitly get the first (0) and last row (-1). To save some space we use the `y` instruction to add both top and bottom border to the last row. We continue to add the gray background grid as well. Instructions are processed top to bottom, meaning later instructions override previous ones. You can see this above with the larger top and bottom borders overriding the previous background borders. `ctrl` `/config/table/cell/border` ```novem-color-border : : a 1 gray-400 -- add a gray grid of size 1 to all sides 0 : b 2 inverse -- select the first row and all columns -- add a size 2 border to the bottom -- using the inverse color (dark on light, -- light on dark) -1 : y 2 inverse -- replicate the above instruction, but -- add the border to the last row and -- use the _y_ instruction to get both -- top and bottom ``` #### Border size Novem tables support up to five different border sizes denoted 1 through 5. 1 is the default and most common while 5 is the largest. It's important to know that larger border takes priority over smaller ones where they intersect. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_border_size` Even though we could have selected the rows and columns using our comma operator `3,5,7,9,11` we have to use a line per instruction in this case. This is because the size operator works on all the listed indices and we want different size. `ctrl` `/config/table/cell/border` ```novem-color-border : : a 1 gray-300 -- continue our grid from above -- but soften the color a bit 3 : b 1 inverse -- add a border to the bottom of all 5 : b 2 inverse -- columns for the given row and size 7 : b 3 inverse 9 : b 4 inverse 11 : b 5 inverse : 3 r 1 inverse -- add a border to the left of all : 5 r 2 inverse -- rows for the given row and size : 7 r 3 inverse : 9 r 4 inverse : 11 r 5 inverse ``` #### Border colors Novem has rich color support across all our visuals. In addition to the named colors and default themes, we also support custom color themes as well as explicit color codes. This is true for our borders just like anything else. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_border_color` Below we've explicitly colored the bottom borders using the 9 color custom scale. The reason we've only used 14 rows is to keep the example consistent with the other tables above. We've left a column in the middle empty for esthetic reasons. `ctrl` `/config/table/cell/border` ```novem-color-border 0 :6 b 2 blue-100 1 :6 b 2 blue-300 2 :6 b 2 blue-400 3 :6 b 2 blue-500 4 :6 b 2 blue-600 5 :6 b 2 blue-700 6 :6 b 2 blue-800 7 :6 b 2 blue-900 8 :6 b 2 blue-800 9 :6 b 2 blue-700 10 :6 b 2 blue-600 11 :6 b 2 blue-500 12 :6 b 2 blue-400 13 :6 b 2 blue-300 0 8: b 2 green-100 1 8: b 2 green-300 2 8: b 2 green-400 3 8: b 2 green-500 4 8: b 2 green-600 5 8: b 2 green-700 6 8: b 2 green-800 7 8: b 2 green-900 8 8: b 2 green-800 9 8: b 2 green-700 10 8: b 2 green-600 11 8: b 2 green-500 12 8: b 2 green-400 13 8: b 2 green-300 ``` #### Border position Position refers to which side of the cell the border will be placed on. Picking the right side can be important to make sure you get the look you want. We've put together the below visualisation to show you how the different instructions lays out the borders. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_border_position` As mentioned above novem borders support seven different position instructions: `lrtb xy a`. The first four should be intuitive as the letters represent `left`, `right`, `top` and `bottom`. `x` and `y` represent the x (row) and y (column) axis and will place the border on all sides in their respective direction. The last instruction is `a` for all which will add border on all sides. `ctrl` `/config/table/cell/border` ```novem-color-border : : a 1 gray-300 -- continue our grid from above -- create the first column - left both and right 0:3 0:3 l 2 inverse -- add border to the left side 5:8 0:3 x 2 inverse -- add border on all vertical (x/column) sides 10:13 0:3 r 2 inverse -- add border on the right side -- create the second column - top both and bottom 0:3 5:9 t 2 inverse -- add border to the top side 5:8 5:9 y 2 inverse -- add border on all horizontal (y/row) sides 10:13 5:9 b 2 inverse -- add border on the bottom side -- create the third column - all, outer and inner 0:3 11:14 a 2 inverse -- create a box with all sides drawn -- our "outside" box 5 11:14 t 2 inverse -- top border 8 11:14 b 2 inverse -- bottom border 5:8 11 l 2 inverse -- left border 5:8 14 r 2 inverse -- right border -- our "inside" box 10:13 11:13 r 2 inverse -- draw right border, one less than all 10:12 11:14 b 2 inverse -- draw bottom border, one less than all ``` #### Border Steps In addition to start and end positions, the novem `slice` operator also supports steps. This makes it easy to alternate colors or group columns and rows into logical entities. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_border_step` The table above shows alternating colors for every two rows and every three columns. This is done by a combination of using the step function of the slicer and offsetting the starting point for the different colors. `ctrl` `/config/table/cell/border` ```novem-color-border : : a 1 gray-300 -- continue our grid from above 1:-1:4 : b 2 gray-500 -- color every 4th row 3:-1:4 : b 2 gray-700 -- repeat but start offset by 2 : 3:-1:6 l 2 gray-500 -- color every 6th column : 6:-1:6 l 2 gray-700 -- repeat, but start offset by 3 ``` #### Border Combinations Finally we have some examples combining all of the above operations in various ways. To save space we haven't included the example code here, but feel free to click the links and read more on the individual chart pages. ##### Header, footer, indices and aggregates *Embedded visualisation:* `/u/novem_demo/p/mtable_config_border_example_1` ##### Border art? *Embedded visualisation:* `/u/novem_demo/p/mtable_config_border_complex` ## Text The text file consists of a newline separated list of formatting instructions controlling cell text styles including font weight, style, size, and family. The novem text instruction has the following composition: ```text row_slice col_slice style ``` Where: - **style** (required): Text style characters or font weight - **size** (optional): Font size (named or numeric) - **family** (optional): Font family #### Text Styles Text styles can be specified as single characters or combined together. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_text_styles` **Style characters:** - `b` for **bold** - `i` for *italic* - `s` for ~~strikethrough~~ - `u` for underline - `r` for regular (no styling, removes underline for links) - `n` for noop (no change) - `-` for blank display (cell appears empty but value is preserved) **Combined styles:** You can combine multiple style characters in a single instruction: - `bi` for ***bold italic*** - `sib` for ~~***strikethrough italic bold***~~ - `ub` for **underline bold** The order of characters doesn't matter - `bi` and `ib` produce the same result. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_text_combined` `ctrl` `/config/table/cell/text` ```novem-color-border 0 : b -- bold the header row 1: : i -- italicize data rows -1 : bi -- bold italic for totals row ``` #### Blank Display The `-` style makes cells display as empty while preserving the underlying data values. This is useful when you want to hide numeric values but still use them for calculations, color scales, or bar charts. `ctrl` `/config/table/cell/text` ```novem-color-border : 1 - -- hide values in column 2 (data preserved) 1: -1 - -- hide values in last column for data rows : : - lg -- blank display with large font (for bar chart labels) ``` **Common use cases:** 1. **Bar charts without visible numbers** - Apply `-` to columns with bar charts when you only want to show the visual bar, not the numeric value. 2. **Color-coded cells** - Hide the raw value but keep the background color that's calculated from it. 3. **Calculations** - The underlying value remains available for color scales, bar chart widths, and other calculations - only the display is affected. **Note:** The blank display affects both HTML and terminal (ANSI) output. #### Font Weights For more precise control over text weight, you can specify numeric values from 100 to 900 (in increments of 100), following the CSS font-weight standard. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_text_weights` **Common weight values:** - `100` - Thin - `200` - Extra Light - `300` - Light - `400` - Regular (same as `r`) - `500` - Medium - `600` - Semi Bold - `700` - Bold (similar to `b`) - `800` - Extra Bold - `900` - Black `ctrl` `/config/table/cell/text` ```novem-color-border 0 : 700 -- bold header (weight 700) 1: : 400 -- regular data rows -1 : 600 -- semi-bold totals ``` #### Font Sizes Novem tables support Bootstrap-inspired named font sizes as well as custom pixel values for precise typography control. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_text_sizes` **Named sizes:** | Size | Pixels | | ------ | ------ | | `xxs` | 10px | | `xs` | 12px | | `sm` | 14px | | `base` | 16px | | `lg` | 18px | | `xl` | 20px | | `2xl` | 24px | | `3xl` | 30px | | `4xl` | 36px | | `5xl` | 48px | | `6xl` | 64px | | `7xl` | 80px | **Special values:** - `n` - Noop (no size change, useful when you only want to set family) **Numeric sizes:** Any number specifies the font size in pixels (e.g., `15` for 15px). `ctrl` `/config/table/cell/text` ```novem-color-border 0 : b xl -- bold extra-large header 1: : n sm -- small data rows (n = noop for style) -1 : b lg -- bold large totals row : : n 14 -- 14px for entire table : : n xxs -- extra extra small (10px) ``` #### Font Families Control the font family using shortcuts or specify custom font names. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_text_families` **Shortcuts:** - `mono` - Monospace font - `sans` - Sans-serif font - `serif` - Serif font **Custom fonts:** Any other value is used as a font-family name (e.g., `Arial`, `Georgia`). `ctrl` `/config/table/cell/text` ```novem-color-border : : n base mono -- monospace for entire table 0 : b lg sans -- bold large sans-serif header 1: : n sm serif -- small serif data rows : : b n mono -- bold monospace, keep current size (n = noop) ``` #### Complete Examples Combining style, size, and family for comprehensive text formatting: *Embedded visualisation:* `/u/novem_demo/p/mtable_config_text_complete` `ctrl` `/config/table/cell/text` ```novem-color-border : : 400 base sans -- base: regular 16px sans-serif 0 : 700 xl sans -- header: bold 20px sans-serif : 0 i sm mono -- first column: italic small monospace -1 : bi lg -- totals: bold italic large : -1 n xs -- last column: extra small ``` ## Align It's often easier to compare numbers and values when they are all aligned in a meaningful way. Novem tables support both horizontal alignment (left, center, right) and vertical alignment (top, middle, bottom). The alignment is applied to a subsection of the table defined by the standard slicing instructions. To instruct the alignment we use these glyphs: ```novem-color-border < -- left align (horizontal) - -- center align (horizontal) > -- right align (horizontal) ^ -- top align (vertical) = -- middle align (vertical) v -- bottom align (vertical) ``` You can also combine horizontal and vertical alignment by using two characters, where the first character is the horizontal alignment and the second is the vertical alignment (e.g., `<^` for left+top). It can be helpful to think of the symbols "pointing" in the direction of the alignment. #### Left Left alignment is the most common alignment and is the default if no instructions are given. That being said, some templates could have a different default direction, so the below example shows how to add a left align to the entire table. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_align_left` All we need to do to left align the table is to create a global selector `: :` and give it the `<` (left) instruction. `ctrl` `/config/table/cell/align` ```novem-color-border : : < -- left align everything ``` #### Center The next example shows how to center all columns in a table except the left one. Often you'll have index values or other information in the left column that you might feel look better left align. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_align_center` Here we start by replicating the left align everything instruction, and then providing a more specific center align `-` that centers the rest of the table. `ctrl` `/config/table/cell/align` ```novem-color-border : : < -- left align everything : 1: - -- center everything except the first column ``` #### Right Finally we have what is probably the most common table alignment for numeric data. Below we have an example that left aligns the right most column, centers the remainder of the top row and right aligns all the numbers below. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_align_right` Here we take advantage of the fact that our rules are processed in order, this allows us to first left align everything, then center everything except the first column and then further right aligning all the numbers. `ctrl` `/config/table/cell/align` ```novem-color-border : : < -- left align everything : 1: - -- center everything except the first column 1: 1: > -- align everything else to the right ``` #### Mix This little alignment mix is more to show the flexibility of the slicers than any common alignment need. Hopefully this can serve as an inspiration to get your table looking just right. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_align_mix` The instructions below primarily does two things: - left center and right align columns 2, 3 and 4 - create three different "snake" patterns for columns 5,6 and 7 Hopefully this helps illustrate that once can create complex formatting with rather few instructions. `ctrl` `/config/table/cell/align` ```novem-color-border : : < -- left align everything : 1: > -- right align all rows and all columns starting -- at column 2 0 1: - -- center top row starting at column 2 : 2 - -- center third column : 3 < -- right align forth column -- create the left "snake" in column five 4:-1 4 - -- center fifth column from fifth row and down 7:9 4 < -- override row 8-10 and left align -- create the right "snake" in column seven : 6 < -- left align column 7 4:-1 6 - -- center seventh column from fifth row and down 7:9 6 > -- override row 8-10 and right align -- create the middle "snake" in column six : 5 - -- start by centering column six 2:-1:4 5 < -- left align every forth row starting at row 3 4:-1:4 5 > -- left align every forth row starting at row 5 ``` #### Vertical Alignment In addition to horizontal alignment, you can control vertical alignment within cells. This is particularly useful when cells have different heights due to varying content. The vertical alignment glyphs are: - `^` - top align (content sits at the top of the cell) - `=` - middle align (content is vertically centered) - `v` - bottom align (content sits at the bottom of the cell) When using a single vertical alignment character, horizontal alignment defaults to center: `ctrl` `/config/table/cell/align` ```novem-color-border : : ^ -- top align everything (horizontal defaults to center) : : v -- bottom align everything (horizontal defaults to center) ``` #### Combined Alignment For full control over both horizontal and vertical alignment, combine both characters in a two-character instruction. The first character is the horizontal alignment and the second is the vertical: ```novem-color-border <^ -- left + top <= -- left + middle ^ -- right + top >= -- right + middle >v -- right + bottom ``` This allows precise positioning of content within cells: `ctrl` `/config/table/cell/align` ```novem-color-border : : <= -- left + middle align everything 0 : -^ -- center + top align header row 1: -1 >v -- right + bottom align data cells except last column ``` ## Padding Padding is the process of adding space on the inside of the cell. Novem supports adding padding in all the cardinal directions, top, down, left and right. The padding is provided as sizes between 1 and 5. The instructions are similar to other novem plot with two slicers followed by direction and size. The direction codes are identical to the border direction codes and are as follows: ```novem-color-border l -- add left padding to cell r -- add right padding to cell t -- add top padding to cell b -- add bottom padding to cell x -- add left and right padding to cell y -- add top and bottom padding to cell a -- add paddings to sides of the cell ``` #### No padding The below table shows a table with no extra padding applied. This creates a compact, condensed look that works well when space is limited. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_padding_none` #### Breathing room Adding extra padding between text and borders makes the table easier to read and gives the content room to breathe. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_padding_room` #### Hierarchies You can use left padding to emulate a hierarchy structure, making it visually clear which rows are sub-items of others. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_padding_hier` ## Format The format file consists of a newline separated list of formatting instructions controlling the cell number/text/date formatting. TODO: EXPLAIN DEFAULT BEHAVIOR #### Numeric The novem platform supports a wider variety of number formatting rules, we actually support the entire [python format mini-language](https://docs.python.org/3/library/string.html#format-specification-mini-language). Number formatting works very similar to other novem formatting instructions, just slice out the part of the table you want to operate on and specify the formatting. `ctrl` `/config/table/cell/format` ```novem-color-format 1:-1 1: ,.1% -- format everything except -- first row and first column as pct -1 1: ,.1% -- format last row as pct (skip first column) ``` #### Scientific Novem extends the standard python library with basic support for scientific notation by using the `s` suffix. This supports a single integer `precision` before the `s` keyword `ctrl` `/config/table/cell/format` ```novem-color-format 1:-1 1: 1s -- format everything except -- first row and first column as pct -1 1: 5s -- format last row as pct (skip first column) ``` #### Money We further expand the standard formatting by offering a convenient "money" format with the `m` suffix. In addition to the precision integer of `s`, `m` also offer an optional currency symbol between the precision and the `m`. `ctrl` `/config/table/cell/format` ```novem-color-format : 0 m -- just m, defaults to precision of 1 : 1 .2m -- a double precision monetary value : 2 $.3m -- add a usd `$` prefix : 3 .4m$ -- add a usd `$` suffix ``` #### Dates *for details see the [python format mini-language](https://docs.python.org/3/library/string.html#format-specification-mini-language)* #### Text #### Markdown Novem tables support markdown formatting in cells. This is particularly useful for adding emphasis, superscripts, or line breaks within cells. ##### Supported Formatting ```novem-color-format **text** -- bold *text* -- italic ^text^ -- superscript ``` ##### Line Breaks To create line breaks within a cell (useful for multi-line headers), you can use: - **Double newline** (`\n\n`) - creates a line break - **Two spaces + newline** (` \n`) - standard markdown hard break This is especially handy for long column headers that you want to split across multiple lines: ```python # Example: Multi-line header using double newline plt.data = """ "Revenue\n\n(USD)","Growth\n\nRate","Year over\n\nYear" 1000,5.2%,+2.1% 2000,3.8%,-1.4% """ ``` The headers will render as: ``` Revenue Growth Year over (USD) Rate Year ``` ##### Combining with Format Instruction To explicitly enable markdown rendering, use the `md` format instruction: `ctrl` `/config/table/cell/format` ```novem-color-format 0 : md -- enable markdown for header row ``` Note: Markdown is also used as a fallback when numeric formats cannot parse a cell value, so text cells with markdown formatting will typically render correctly without explicit configuration. #### Null Values By default, null/missing values in tables are rendered as invisible whitespace. You can customize how null values are displayed by adding an optional `na_rep` token after the format type. **Syntax:** ```novem-color-format row_slice col_slice format_type [na_rep] ``` **Examples:** `ctrl` `/config/table/cell/format` ```novem-color-format : : $.2m N/A -- money format, nulls show "N/A" : 2 %m/%d - -- date format, nulls show "-" 0 1 st #N/A -- static text, nulls show "#N/A" ``` **Blank convention:** To explicitly show invisible whitespace (the default behavior), use: - `_` - invisible whitespace - `blank` or `BLANK` - same as `_` (case-insensitive) `ctrl` `/config/table/cell/format` ```novem-color-format : : .2f _ -- float format, nulls show blank (explicit) : : st blank -- static text, nulls show blank (explicit) ``` Any other string is used literally as the null value display text. *Embedded visualisation:* `/u/novem_demo/p/mtable_config_format_na_rep` ## Overflow By default novem tries to assign width according to column content, however sometimes there just isn't enough space. When that occurs one of two things can happen: - Ellipsis `e` - the substitution of the excess content with ellipsis `...` - Truncation `t` - the removal of the excess content - Wrap `w` - the splitting of content across multiple lines - Overflow `o`- have the content spill across to the other cells #### Ellipsis The default behaviour is `e` (ellipsis) on overflow. `ctrl` `/config/table/cell/overflow` ```novem-color-format : : e -- the default overflow strategy ``` #### Truncation Truncation simply cuts off the words with no further indication to the user that information is missing. However, unlike ellipsis, words are never partially truncated but removed in full. The only exception is if there is only one word, in which case it will overflow. `ctrl` `/config/table/cell/overflow` ```novem-color-format : : t -- truncate the entire table ``` #### Wrap Wrapping allows the text to continue to wrap onto the next line expanding the row until it shows all the text `ctrl` `/config/table/cell/overflow` ```novem-color-format : : w -- wrap the entire table ``` #### Overflow The default behaviour is `e` (ellipsis) on overflow. `ctrl` `/config/table/cell/overflow` ```novem-color-format : 0 w -- wrap all rows the first column : 1: t -- truncate the rest of the table ``` ## Width Novem tables allow you specify column widths in "units". You can think of units almost like pixels, except there is no guarantee that a unit is a 1:1 match against screen pixels. You can however assume that relative sizes given in units across novem endpoints is consistent. So a medium size table of 600 units can be properly divided into three columns of 200 units. There are three types of widths you can assign to a column, `min`, `max` and `fixed` shortened `i`,`x` and `f` respectively. Width instructions follows a similar slicing syntax as other cell instructions, but only column selectors are supplied, followed by width in units and then type. #### Max width Max width `x` allows you to define the maximum width a column can have, this means the column can be smaller, but never wider, than the supplied width. `ctrl` `/config/table/cell/width` ```novem-color-border : 0 x 200 -- give the first column a max width of 200 ``` #### Min width Minimum width `i` defines the smallest width a table column will have, regardless of content the width of the column will never be smaller than the specified minimum width. `ctrl` `/config/table/cell/width` ```novem-color-border : 1 i 200 -- give the second column a min width of 200 ``` #### Fixed width Fixed width specifies that a column should never have any other size than the supplied width, all content larger than the width will be truncated according to formatting rules. `ctrl` `/config/table/cell/width` ```novem-color-border : 2 f 200 -- specify that the third column should be fixed to 200 units ``` ## Merge Merge operations are a presentation change only. The underlying table data is still expected to be present in an MxN matrix format. That being said, the underlying data can be empty cells as its content will be ignored. Novem allows you to merge table cells together for more complex layout such as multi column or multi index views. When merging cells the styling, formatting and content of the merged cell will be the value of the left, top most cell. Content and formatting of all the other cells in the merged set, except width, will be discarded. The merging operation is the last operation applied to the dataset, meaning no other instructions will have to be adjusted to account for the merged layout. Merging cells is similar to other novem slicing operations in that a slicing selector is created to define the cells to operate on. Unlike other table cell options the only action is to merge, so no further parameters are needed. In addition, as you can only merge a continuous selection of cells, [step operations](/docs/reference/api/slicing.md#step-instructor) are not allowed in novem merge selectors. #### Creating category columns A common way to use cell merges is to delineate different categories where a common set of sub-titles is shared amongst super titles. Novem cell merging allows you to easily create these more complex layouts. `ctrl` `/config/table/cell/merge` ```novem-color-border 0 1:6 cat1 -- Merge category 1 0 7:12 cat2 -- Merge category 2 ``` #### Creating a multi index (TODO) A common way to use cell merges is to delineate ## Priority Novem tables are made to be shown on many different displays, from small cellphone widgets to landscape orientation documents. To give the user better control over what information is to be included in the different views we support a priority syntax for rows and columns. Priority is designated by regular slicing notation followed by a priority integer and a transformation option. The lower the number the higher the priority, by default all cells start with an equal priority of `0` and the option of collapsible. **Note: mail tables `mtable` does not support priority** The mail table renderer does not have access to width information at run-time and as such cannot adjust to the display format. Please exercise care when designing for e-mails. #### Hidden Rows and columns given the hidden `h` attribute will simply disappear if there are not enough room to show them. The table will appear as if it only contains the rows and columns the users can see in their current viewport. `ctrl` `/config/table/cell/priority` ```novem-color-border : : 0 h ``` #### Collapsed Collapsed columns and rows, designated with the `c` attribute, will give a visual indication that one or more row/columns have been hidden in the user view. `ctrl` `/config/table/cell/priority` ```novem-color-border : : 0 c ``` ## Chart The chart file allows you to add horizontal bar charts inside table cells. This is useful for visualizing relative values, progress indicators, or diverging data directly within your table. Chart instructions follow the standard novem slicing syntax with additional parameters for chart type and scale configuration. **Note:** Bar charts completely replace the cell content. The numeric value is not displayed alongside the bar - only the bar itself is shown. If you need to display values, consider using a separate column. #### Instruction Format ```text row_slice col_slice chart_type [scale_spec] [opts] ``` Where: - **row\_slice**: Row selector (standard slicing syntax) - **col\_slice**: Column selector (standard slicing syntax) - **chart\_type**: Currently only `hbar` (horizontal bar) is supported - **scale\_spec**: Optional scale configuration in the format `(min,max)^scale` or `(min,mid,max)^scale` - **opts**: Optional flags: - `l` - display value labels - `h` - hide center divider (for diverging bars) #### Scale Specification The scale specification defines how values are mapped to bar widths: ```novem-color-format (0,100)^lin -- Simple scale from 0 to 100, linear (_,_)^lin -- Auto-detect min and max from data (_,0,_)^lin -- Diverging scale with 0 as midpoint (auto min/max) (-50,0,50)^lin -- Explicit diverging scale ``` **Special values:** - `_` - Auto-detect from data (min, max, or both) - Any number - Explicit value for that position **Scale types:** - `lin` - Linear scale (currently the only supported scale type) #### Simple Horizontal Bars Simple bars grow from left to right based on the value's position within the specified range. *Embedded visualisation:* `/u/novem_demo/p/mtable_chart_hbar_simple` `ctrl` `/config/table/cell/chart` ```novem-color-format 1: 1 hbar (0,100)^lin -- Add bars to data rows, column 2 -- Scale from 0 to 100 ``` Values are normalized to the range: - A value of 0 produces no bar - A value of 100 produces a full-width bar - Values outside the range are clamped #### Auto-Range Bars Let novem automatically determine the range from your data: `ctrl` `/config/table/cell/chart` ```novem-color-format 1: 1: hbar (_,_)^lin -- Auto-detect min and max from the data -- Applies to all data rows and columns ``` This is useful when you don't know the data range in advance or want bars to always fill the available space proportionally. #### Diverging Bars Diverging bars are ideal for showing positive and negative values, year-over-year changes, or any data with a meaningful midpoint. *Embedded visualisation:* `/u/novem_demo/p/mtable_chart_hbar_diverging` When a midpoint is specified, bars grow in opposite directions: - Values below the midpoint grow **left** from center - Values above the midpoint grow **right** from center - A vertical divider marks the midpoint `ctrl` `/config/table/cell/chart` ```novem-color-format 1: 1 hbar (_,0,_)^lin -- Diverging bars with 0 as midpoint -- Min and max auto-detected from data ``` This is particularly effective for displaying: - Year-over-year percentage changes - Profit/loss figures - Sentiment scores - Any metric with positive and negative values #### Combining with Colors Chart bars inherit colors from the [dynamic color configuration](/docs/reference/plot/config/colors.md#dynamic-colors "dynamic colors"). When both chart and color instructions target the same cells, the bar will be rendered in the foreground color specified by the color config. `ctrl` `/config/table/cell/chart` ```novem-color-format 1: 1: hbar (_,0,_)^lin -- Add diverging bars ``` `ctrl` `/config/colors` ```novem-color-format 1: 1: fg bad,neutral,good(_,0,_)^lin -- Color bars by value ``` This creates bars that are colored based on their value - red for negative, gray for neutral, and green for positive (using the bad/neutral/good palette). *Embedded visualisation:* `/u/novem_demo/p/mtable_chart_hbar_with_colors` #### Multiple Chart Instructions You can apply different chart configurations to different parts of your table: `ctrl` `/config/table/cell/chart` ```novem-color-format 1: 1 hbar (0,100)^lin -- Progress bars for column 2 1: 2 hbar (_,0,_)^lin -- Diverging bars for column 3 ``` Instructions are processed in order from top to bottom. Later instructions override earlier ones for overlapping cells. #### Labels You can display the formatted cell value as a label within the bar by adding the `l` flag after the scale specification: *Embedded visualisation:* `/u/novem_demo/p/mtable_chart_hbar_simple_labels` *Embedded visualisation:* `/u/novem_demo/p/mtable_chart_hbar_diverging_labels` `ctrl` `/config/table/cell/chart` ```novem-color-format 1: 1 hbar (0,100)^lin l -- Bars with value labels 1: 2 hbar (_,0,_)^lin l -- Diverging bars with labels ``` **Label positioning:** - By default, labels appear **outside** the bar for small bars (< 50%) and **inside** the bar for larger bars (>= 50%) - Labels inherit text formatting (font, size, bold, etc.) from the cell - Labels use `color: inherit` to work with any background **Alignment overrides:** When alignment is applied to the cell, it controls label positioning: | Alignment | Effect | | ------------ | --------------------------------------- | | `<` (left) | Label at left edge, inside bar | | `>` (right) | Label at right edge, inside bar | | `^` (top) | Label at value edge (outer edge of bar) | | `v` (bottom) | Label at root edge (inner/center edge) | Labels respect the cell's overflow rules - use ellipsis/truncation as needed. #### Hiding the Center Divider Diverging bar charts display a 1-pixel vertical line at the midpoint (center) to visually separate positive and negative values. You can hide this divider by adding the `h` flag: `ctrl` `/config/table/cell/chart` ```novem-color-format 1: 1 hbar (_,0,_)^lin h -- Diverging bars without center divider 1: 2 hbar (_,0,_)^lin lh -- Diverging bars with labels, no divider ``` The `h` flag only affects diverging bars (those with a midpoint). Simple bars don't have a center divider, so the flag has no effect on them. You can combine `l` and `h` in any order: `lh`, `hl`, or as separate words `l h` all work the same way. **Hiding labels with blank text style:** If you want bar charts with the `l` flag but need to hide specific labels, use the `-` (blank) text style. This hides the formatted value while preserving the bar chart calculation: `ctrl` `/config/table/cell/chart` ```novem-color-format 1: 1 hbar (0,100)^lin l -- Bars with labels ``` `ctrl` `/config/table/cell/text` ```novem-color-format 1: 1 - -- Hide labels in column 2 ``` This shows the bar but not the numeric label - useful when space is limited or you want a cleaner visual appearance. #### Terminal Output Bar charts are fully supported in terminal/ANSI output. When viewing tables via `novem -p plot_name -x` or through the API's ANSI endpoint, bars are rendered using Unicode block characters for half-character precision. Note that labels are only supported in HTML output, not terminal output. #### Complex Layout Combining charts with merging, colors, width constraints, and text formatting allows for sophisticated dashboard-style tables. *Embedded visualisation:* `/u/novem_demo/p/mtable_chart_complex_layout` ## Axis > **Note:** the axis grammar is early access. You can explore it interactively in the > [plot playground](/learn/playground), which implements the grammar described on this page. The axis grammar lives in one file directly under your plot's config (margins have [their own file](/docs/reference/plot/config/margins.md) beside it): ```bash plot_name ├── config │ ├── axis => The axis grammar (this page) │ ├── margins => Surface margins (own format) ``` An axis is a stack of **lanes** over one scale. Each non-empty line of the `axis` file is one lane: a filter that selects domain values, plus properties that say what to draw for them — marks, labels, a gridline, a shaded band. Months and years under a time axis are two lanes; a grid is a lane; a recession band is a lane. ### The line grammar ```text [+] [domain] [selection] [key:value ...] ``` Two positional filter slots, like d3's domain/range split: the **domain** declares the axis's coordinate span, the **selection** picks values within it for this lane's furniture. One filter token is always the selection; the domain slot exists only in the two-token form: ```novem-axis x 2000:2030 ::5y f:%Y -- domain: 30 years; label every 5 across it y 0:100 ::25 f:,.0f -- domain 0-100, ticks every 25 x ::5y f:%Y -- selection only; domain stays at its default ``` - Lines are whitespace-tokenized. `--` starts a comment. Blank lines are ignored. A lane dies only with its axis token: an invalid property is dropped while the rest of its line survives, and so is an invalid filter, whose slot falls back to auto. A half-typed key reports as one, so adding a property to a line never blanks the axis mid-keystroke. - Values containing whitespace are quoted — `'…'` or `"…"`, the opener naming the closer: `f:"%b %Y"`, `l:'Net flow'`. - Keys and axis tokens are case-insensitive; formats are case-sensitive. Category names match case-insensitively with spaces as underscores. - The filter is optional and defaults to `:` (everything / auto). | Axis token | Meaning | | ------------------------------ | --------------------------------------------------------- | | `x` … `xN` | x-family rails (bare `x` ≡ `x1`); default side bottom | | `y` … `yN` | y-family rails (bare `y` ≡ `y1`); default side left | | `o` | the outer frame (domain lines on all four sides) | | `+ xN` `+ yN` (or glued `+xN`) | amend the previous label lane of that rail (see Amending) | Numbers are rail ordinals, not sides — a rail is a full axis, and there is no upper bound. The side is the `e:` property on each line (`e:l|r` on y, `e:b|t` on x; defaults left/bottom): lanes partition by (family, side) before any rail rule applies, so `e:r` on one line never moves another line's furniture, and ordinals count per side (`y2 e:r` is the second rail of the right side). Rails sharing a side stack visibly outward in ordinal order. The primary rail — the innermost rail of the default side, or of the only side present — owns the plot scale; every other rail inherits it (ruler-style stacking) or declares a numeric domain of its own — a *reading rail* that relabels the same pixels (°F beside °C) without ever re-windowing the plot. Special forms: `y e:r mirror` duplicates the opposite side's innermost rail on this side (crossed-axis offsets swap, so outward space stays outward); `x none` owns the axis and renders nothing (suppresses the default axes, see Defaults); `cut` turns a lane's marks into holes instead of ink (see Cut lanes). #### Cut lanes `cut` makes the lane's marks paint **holes** through everything the plot has drawn below the lane — series, gridlines, chrome — instead of ink. A hole reads as the page background with no colour named, so a cut divider is correct in every theme, in dark mode, and over an unknown host page by construction, where a `c:`-painted line would need the right colour picked for each context. Unlike `none` and `mirror`, `cut` is a mode, not a whole-lane claim: it is a bare keyword on an ordinary rail line, written anywhere among the filters (`x cut ::2` and `x ::2 cut` are the same lane), and the lane's filters, slices and geometry properties compose exactly as on a painted lane — `ta:` makes a dashed hole, `tw:` sets its width, and the lane's labels still render normally. The shipped price/volume divider is one line: ```novem-axis y2 ts:-100% z:over cut tw:1 -- a full-bleed divider that cuts through ``` - By default the cut removes from the entire stack below the lane; `z:over` states that intent explicitly. An explicit `z:under` scopes the holes to bands and the under-plot gridlines only — legal, but nearly invisible since the series sits above it, and diagnosed as such. - Mark colours (`tc:`, `dc:`, `fill:`) have no ink to colour on a cut lane and drop with a diagnostic; `c:` survives, since it still colours the lane's labels. - `cut` is not amendable (a `+ x` line drops it), does not combine with `none` or `mirror`, and is not supported on the frame `o`. ### The domain The domain slot is a single stepless range (`a:b`, `_`, or `:`); endpoints are literals, value tokens, or `_` (data-anchored). It is axis-level (last declaration wins; all rails of a family share rail 1's plot domain), and declaring one replaces the default — the zero-config domain is the data extent, zero-clamped on value axes, so `y i:x :` gives a terminal-style data-tight scale and `y 50:200 :` is the zoom. Data outside the domain clips; a domain wider than the data is legal and central (projection space). Selections can never re-window a chart — only domains can. Bars and areas truncate at the window edge: under a domain that excludes zero the base rests on the edge instead of projecting toward raw zero, so `y _ :` over strictly positive data gives truncated bars. ### Selections A selection is a comma-separated union of atoms, written in the axis's own domain units — numbers on value axes, dates on time axes, category names or positions on categorical axes. | Atom | Example | Selects | | --------------- | ------------------------- | ----------------------------------------------------- | | `:` (or absent) | `x` | everything — nice ticks, or every category | | `~N` | `y ~4` | about N automatic ticks | | literal | `y 0` | one value | | union | `y 0,50,100` | several values | | | `x 2026-03-15,2026-11-01` | explicit dates | | | `x equities,bonds` | named categories | | range | `y 50:90` | automatic ticks within 50–90 | | range + step | `y 42:96:25` | 42, 67, 92 — explicit start anchors the phase | | | `y ::25` | step-25 multiples across the whole domain | | | `x 2024-01:2026-07:q` | quarterly boundaries between two dates | | | `x ::q` | quarterly across the whole range | | | `y ::*10` | multiplicative: 1, 10, 100, … (from the domain start) | | bare step | `x q` | shorthand for `::q` | **Steps** are numbers on value axes, frequencies on time axes (`s`, `min`, `h`, `d`, `w`, `m`, `q`, `y`, with multiples like `15min`, `6h`, `2w`, and `2q`, and anchors like `y@oct` for fiscal years or `w@mon` for week start), and integer positions on categorical axes (`0:_:5` = every 5th category; negatives count from the end, so `0:_:5,-1` adds the last). `sp:` controls the spacing independently of the ticks. Use the matching period spacing when each second, minute, hour, or week should occupy one equal-width slot (including empty periods between data points): ```config/axis x s f:%H:%M:%S sp:s x min f:%H:%M sp:min x h f:"%b %d %H:%M" sp:h x w f:"%b %d" sp:w ``` **Value tokens** stand in for data-derived values anywhere a value can appear: `i` (min), `x` (max), `e` (median), `a` (mean), `f` (first row), `l` (last row), `_` (contextual endpoint). `y i,x` labels only the data extremes. **Filters select from your data, not from the calendar.** A monthly filter over business-day data snaps to real rows and never emits a weekend tick; periods with no data emit nothing. #### Wide domains and sparse data Period selections partition the domain; each boundary snaps to a real row when its period contains one. Dataless periods inside the data still emit nothing (a daily filter never ticks a weekend), but periods entirely outside the data extent emit their calendar boundaries — so `x 2000:2030 ::5y f:%Y` stays labelled across an axis far wider than the data. Range amends pair with this: `+ x i:x tx:b` emphasizes the labels where the data actually lives, and `x x:_ fill:gray-100` shades from the data max to the domain end. > **Note:** `f` and `l` cannot start a range — `f:…`/`l:…` read as the format and title properties. Use `i:x` (data min > to max), which selects the same values on a sorted axis. ### Properties Line-local unless marked *axis-level* (last one wins across the axis). Prefixes name the thing (`t` tick, `tl` tick label, `d` domain, `l` the axis's own label); suffixes name the property (size, width, color, dash-array, position, format, overflow, rotation, backing, extent). `c:` and `tx:` are generic: they style whatever the line draws. | Key | Values | Meaning | | ----------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `an:` | value, token, `edge`, rail(value) | Anchor: where the lane sits, as a value in the cross axis. `an:0` = the zero line (x default), `an:100` = rebased charts, `an:e` = the median. The rail form translates through a named rail's scale — `x y(0) dw:2` is an explicit zero rule, `an:y2(32)` anchors in a reading rail's units; bare `y(…)` in a filter position is shorthand for `an:`. | | `af:` | `auto` `a` `0` | Tick label stagger policy — the [labels grammar's](/docs/reference/plot/config/labels.md) lane key: `af:a` forces the alternate two-row stagger (`tlo:a` is the compact alias, same wire), `af:0` turns a standing stagger off, `auto` is the default ladder. | | `att:` | `start` `end` | Which real datum represents a period boundary: Jan 1 (`start`) or Dec 31 (`end`). | | `bp:` | inner\[,outer\[,outer-end]] | *axis.* Band spacing on ordinal scales (categories, or time with `sp:index`): the gap between bands/groups, then outer padding at the ends. Values are `%` of the step or bare px; one outer value is symmetric, two are start,end (`bp:10%,0,25%` is flush left, padded right). Bar defaults: `7%,20%`. | | `bpi:` | value | *axis.* Within-group gap for grouped bars — the spacing between one group's members. `%` of the group slot or px: `bpi:25%`, `bpi:2px`. | | `c:` | color\[,color] | Base color for whatever the line draws (labels; the fallback for `tc:`/`dc:`). Optional dark-mode variant after the comma. | | `da:` | px list | Domain dash array: `da:2,2`. (Tick dashes are `ta:`.) | | `dc:` | color\[,color] | Domain line color for this side; falls back to `c:`. | | `de:` | `window` `data` | Domain line extent: plot edge to edge, or first to last datum. | | `dir:` | `rev` | *axis.* Reverse the domain (rank 1 at the top). | | `ds:` | px or `%`, signed | Domain edge-tick size: end caps at the two ends of the domain span, sign and `%` form as `ts:` (`ds:-100%` spans the plot). Caps render whenever `ds:` is set — no domain line required. Caps are domain furniture: they grow the margin when needed but never push the label rows — labels stay attached to the ticks. | | `dw:` | px | Domain line width for this side. | | `fill:` | color\[,color] | Makes the line a shaded band across the plot (see Bands). | | `l:` | quoted text | *axis.* The axis's own label — the title. | | `lp:` | `start` `mid` `end` `top` | *axis.* Axis label placement. `top` (y default) is the horizontal unit line above the tick labels. | | `mb:` | signed px or em | Offset: extra space below what this lane draws. | | `ml:` | signed px or em | Offset: extra space left of what this lane draws. On an x label lane it nudges the labels along the axis (right); `mr:` nudges left. | | `mr:` | signed px or em | Offset: extra space right of what this lane draws. | | `mt:` | signed px or em | Offset: extra space above what this lane draws — on a label lane it moves the labels only (tick padding); on a mark or line lane it moves the marks or line. On a y label lane `mt:`/`mb:` nudge the labels along the axis. Negative pulls closer. | | `p:` | px or `%` | *axis.* Inner padding, both ends of this line's family — `x p:8` is `pl:8 pr:8`, `y p:8` is `pt:8 pb:8`. | | `pl:` `pr:` | px (default) or `%` of the span | *axis.* Inner padding of the x scale's left/right end: the numerical mapping insets, so marks AND ticks move in together while the frame and domain lines stay put (`m*` is the furniture tool). An unstated end already reserves whatever the marks at that end need — the radius, its outline, and a pixel of daylight — so a scatter point on the domain edge keeps its whole dot; write a value to override that side. Category x keeps `bp:`; under `sp:` spacing the padding insets the whole slot span and `bp:` composes within it. | | `pt:` `pb:` | px (default) or `%` | *axis.* Same for the y scale's top/bottom end, including the mark-extent default. | | `s:` | col-slicer | *rail.* Claim series onto this rail's scale: `y2 e:r s:-1 0:_` plots the last series against its own right-hand axis. Column vocabulary from the color grammar (indices, ranges, lists, headers); last claim wins, later rails steal, unclaimed series stay on the primary rail. On the **x family** a claim reads the series *slots* instead: `x s:: f:auto ts:6 dw:1` ticks every band's slot centers with the series headers — the natural axis for a single-band grouped chart, such as a `by 0` facet panel. Stacked series share a slot and tick once; scalar x has no slots and diagnoses. | | `sc:` | `lin` `log` | *axis.* Scale transform. | | `sp:` | `time` `index` freq | *axis.* Time spacing. `time` (default): calendar-continuous. `index`: one step per ROW — trading-day charts lose their weekend gaps. A frequency (`sp:s`, `sp:min`, `sp:h`, `sp:d`, `sp:w`, `sp:m`, `sp:q`, `sp:y`) is period-normalized: every period one equal-width slot, data positioned within it — months render the same width regardless of day counts, and a lone point a year out still sits twelve slots away. This is also the expected x domain used by marks `na:`: `index` expects only observed/shared rows, period modes provide exact calendar slots, and continuous time derives the regular observation cadence. Both ordinal modes put bars on band geometry, so `bp:`/`bpi:` work on time axes. | | `ta:` | px list | Tick dash array: `ta:1,3` for a dotted grid. | | `tc:` | color\[,color] | Tick/mark color; falls back to `c:`. | | `lbc:` | `auto`, `0`, color | Tick-label background color (`tlb:` remains an alias). Tick labels, including `tls:in`, are bare by default; a value explicitly opts into the plate. | | `tlf:` | format, `auto` | Tick label format (alias: `f:`, the everyday shorthand); presence makes the line a label lane. Number specs (`,.0f`, `+,.1f`, `.0%`, `.2s`), strftime plus the novem date tokens (`%o` ordinal day — "1st", "22nd"; `%oq`/`%ow` ordinal quarter and ISO week; `%q` quarter; `%fy` fiscal year), and templates like `f:"{,.0f}bn"`. | | `tlo:` | `e` `t` `w` `o` `a` `r` | Tick label overflow: ellipsis, truncate, wrap to the band, overflow, alternate across two staggered rows (`af:a` is the canonical stagger spelling; `tlo:a` stays the compact alias), or `r` — auto-rotate on overlap: labels stay horizontal while they fit and rotate to the smallest angle (30/45/60/90°) that does. | | `tlp:` | `start` `mid` `end` | Tick label position along the domain — which side of its mark or span the text sits. On a y lane this is vertical: `tlp:end` is "just above the gridline". | | `tlr:` | degrees | Tick label rotation, −90 to 90 (also chosen automatically by `tlo:r`). The attachment reuses `tlp:` — `start` attaches the reading start at the tick and grows down-right (the auto-rotate default), `end` grows up-right into the tick, `mid` centers on it; without `tlp:` the sign controls direction. | | `tls:` | `in` `out`\[,px] | Tick label side across the axis: outside the plot (default) or inside the plot area. | | `tp:` | `on` `between` | Marks on the datum, or at span/band boundaries. | | `ts:` | px or %, signed | Tick size. Marks exist only when `ts:` is declared — a bare label lane draws labels and nothing else. Positive grows away from the plot, negative grows into it; `ts:-100%` is a full-bleed gridline. | | `tw:` | px | Tick stroke width. | | `tx:` | style/size/family, any order | Text style for whatever the line draws: style chars `rusbin` or a weight `100`–`900`, named sizes `xxs`–`7xl` or px, family `mono`/`sans`/`serif` — comma-separated tokens, each optional, any order. | | `sub:` | `25` `2` `5` `0` | Sub-decade log minors — the conventional 2s/5s furniture in one key (`25` = both families); meaningful on `sc:log` lanes. | | `z:` | `over` `under` | Paint a gridline lane above the data (grid-over-bars styles) or below it; `under` spells the default explicitly. On a cut lane an explicit `z:under` scopes the holes to bands and the under-grid (see Cut lanes). | ### Labels Labels have two positioning dimensions. `tlp:` moves text **along** the domain; `tls:` moves it **across** the axis: ```novem-axis y ::20 ts:-100% c:gray-200 -- gridlines y2 e:r ::20 f:,.0f ts:0 tls:in,3 tlp:end -- labels on the right, inside the plot, sitting just above their line ``` Labels placed inside the plot get an automatic background plate so they stay legible over gridlines and bars (`tlb:0` disables it). The first and last labels of a lane snap inward automatically when centring would hang past the plot edge; an explicit `tlp:` opts out. Every line with a format and `tls:out` claims the next label row outward, so two label lanes make a two-row axis: ```novem-axis x m f:%b x y f:%Y tlp:start -- Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec -- 2026 ``` #### Amending a lane An axis token with `+` edits labels **inside the previous label lane's row** instead of opening a new one. Where the base lane has a label at the selected value the amend overrides it; elsewhere it inserts. Within an amend filter, `f` and `l` mean the base lane's first and last label: ```novem-axis x y f:'%y tlp:start + x f f:%Y -- first year in full: 2019 20 21 22 y 0:_:200 f:,.0f ts:0 + y l f:"{$,.0f} billion" -- unit on the top label only + x 2020,2024 tx:b -- no format: just bold those two labels ``` Use `:` to match the base lane's complete rendered set, then a second positional token to slice those results. Negative positions count from the end: ```novem-axis y 160:240 ~5 f:$,.0f ts:0 + y : -1 c:red-500 f:"{$,.0f} per share" ``` #### Titles `l:` sets the axis title, `lp:` places it. `lp:top` (the y default) renders a horizontal unit line above the labels; `start`/`mid`/`end` run along the axis: ```novem-axis y l:"Percent of GDP" lp:top x l:"GDP per capita →" lp:end ``` The title takes the [labels grammar's](/docs/reference/plot/config/labels.md) letters as satellites — the same keys place a data label, a strip label and a title, so you learn them once: ```novem-axis y l:"Streamflow" lp:mid lr:-90 -- the classic vertical y title y l:"Index" lp:mid lr:-90 le:r -- ...docked on the right edge y l:"per capita" lp:end li:in -- overlaid inside the plot y l:"Price" le:tl li:in lm:5 -- inside the top-left corner x l:"Month" le:t lm:6 -- above the plot, 6px off ``` | key | values | meaning | | --------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `le:` | `l` `r` (y) / `b` `t` (x), corners `tl tr bl br` | the edge the title docks; a corner fuses in the along-position, spelled with the axis's own `lp:` (`le:tl` on y is `le:l lp:end`); `lp:top` is a position, not an edge, and keeps its spot | | `li:` | `in` `out` | inside the plot area (overlay, claims no margin) or outside the furniture | | `lm:` | px or `%`, pair `across[,along]` | the offset from the default spot; unsigned is exact, explicit `+`/`-` adjusts the zero default | | `lml:` `lmr:` `lmt:` `lmb:` | px | per-side margins on the title box — unsigned exact, `+`/`-` relative to zero; they compose with `lm:` | | `lmx:` `lmy:` `lma:` | px | side bundles under the same absolute/relative rule; a later single side overrides its half | | `lo:` | `e` `t` `w` `o` `h` `f`, full words | overflow vs the axis span: ellipsis, truncate, wrap, overflow, hide, flip | | `lw:` `lwi:` `lwx:` | px | the size trio on the title (CSS clamp, min beats max): `lwx:`/`lw:` cap the title below the axis span, with `lo:` deciding how the excess dies; `lwi:` floors it | | `lr:` | degrees −90..90 | rotation; `lr:-90` reserves one line of margin instead of the text width | | `lb:` | `auto`, `0`, color | a legibility plate behind the title | ### Marks, gridlines and paint order A gridline is a tick that crosses the plot: `ts:-100%`. When two lines of the same axis put a mark at the same value, the later line's mark **replaces** the earlier one — restyle a single gridline by overpainting it: ```novem-axis y ::25 ts:-100% c:gray-200 y 0 ts:-100% c:gray-500 -- the zero line wins ``` Paint order is fixed: bands, then gridlines, then your data, then `z:over` gridlines, then ticks, domain lines and the frame, then labels and titles. ### Bands `fill:` shades the filtered interval(s) of the domain across the plot: ```novem-axis x 2020-02:2020-04,2022-06:2022-10 fill:gray-100,gray-800 -- two bands y 2:3 fill:green-100,green-900 -- a target range x ::10y fill:gray-100 -- alternating decades ``` Explicit ranges shade exactly those spans; stepped filters shade **alternating** periods (zebra). Bands paint below everything and never widen the window. ### Defaults An empty axis file behaves as this implicit program — a sketch, not a paste-able file: `~auto` stands in for the automatically chosen tick count, whereas in a real file `~` always takes a number (`~5`): ```text x : f:auto an:0 ts:6 dw:1 y ~auto f:auto an:edge ts:6 dw:1 ``` **Touch an axis, own it:** your first line for an axis replaces that axis's defaults entirely — `x m f:%b` gives month labels and nothing else on x; add `dw:1` back if you want the domain line. Other axes keep their defaults. `f:auto` keeps the automatic format while you change placement or style. ### Examples FT-style index chart: ```novem-axis y 60:140 ::20 ts:-100% c:#ccc1b7 y 100 ts:-100% c:#66605c -- index base line y2 e:r ::20 f:,.0f ts:0 tls:in,3 tlp:end y2 l:"Index, 2019 = 100" lp:top x q f:%b ts:3 x y f:'%y tlp:start + x f f:%Y x 2020-02:2020-04 fill:#e8dcd2 ``` Fiscal-year briefing chart: ```novem-axis y -8:_ ::4 f:,.0f ts:0 tx:600,2xl y 0 ts:-100% c:inverse y l:"Percent of GDP" lp:top x 2000:2036:4y@oct f:%fy tx:600,2xl x 2026-10 ts:-100% ta:6,3 c:gray-500 x 2026-10:_ fill:gray-100 ``` Extremes-only minimal axis: ```novem-axis y i,x f:,.1f ts:4 dw:1 de:data x f,l f:"%b %Y" ts:0 ``` Log decades: ```novem-axis y 1:_ ::*10 f:,.0f sc:log ``` Rankings, best on top: ```novem-axis y 1:10 ::1 f:,.0f dir:rev ``` ### Margins Explicit plot margins live in their own file, `config/margins` — see the [margins page](/docs/reference/plot/config/margins.md). What an axis author needs to know: an explicit margin is exact and doubles as the wrap budget for `tlo:w` labels; unstated sides are `auto`, the measured autocompute. ## Labels > **Note:** the labels file is part of the early-access flat config tree. You can explore it interactively in the > [plot playground](/learn/playground). Where a [legend](/docs/reference/plot/config/legend.md) describes a whole series, a label describes one point. Labels live in one file directly under your plot's config: `plot` `config/labels` One instruction per line: ```novem-labels [slicer [slicer]] [prop ...] ``` Every prop is a `k:v` pair, and the label text itself is the value of the `l:` prop. Labels never appear uninvited: an empty file draws nothing, and a file containing just `l:%v` gives correct value labels on any chart — above columns (sign-aware), centered in stacked segments and slices. Props split into two namespaces. Bare keys are line-scoped instructions: the selector, the text (`l:`), the lane policy (`af:`), `total` and `hide`. Everything that styles the **label box** carries the `l` prefix — `le:` `li:` `la:` `lm:` `lo:` `lr:` `lc:` `lb:` `lbs:` `lbd:` `lbp:` `ltx:` and the size keys — one `l*` vocabulary shared with facet strips, axis titles and complications, on the model of the axis `t`/`tl` prefix. The old bare spellings (`e:`, `m:`, `c:`, ...) are retired and error with a pointer to their `l*` replacement. ### Selecting points The datapoint is the unit: a cell (row, column) on bars, lines and scatter, a slice (row) on pie and donut. Selection is the shared slicer vocabulary, aimed at points: ```novem-labels l:%v le:c -- no slicer: every datapoint espresso l:%v -- one slicer: a column (a series) -1 : l:%s le:r -- two slicers: rows x cols of the raw csv 1,-1 : l:%v -- first and last data rows ``` One slicer addresses **columns**; two are rows × columns of the RAW csv layout ([the slicing contract](/docs/reference/api/slicing.md)): row 0 is the header line, col 0 the x column — both paint nothing — so the first data row is `1` and the first series is column `1`. Ranges are stop-inclusive, negatives count from the end, header names substitute in the column token. Pie and donut slices are ROWS: `2 : l:%v` labels the second slice. Under facets, labels resolve per panel against the panel's own data. #### Conditional selection The row slot also takes a **value token**: `@x` the maximum, `@i` the minimum, `@f` the first, `@l` the last cell with a value. Resolution happens server-side, per selected series, so each series gets its own answer: ```novem-labels @x : l:%v lc:mark -- every series' peak, series-colored @x -1 l:"peak %v" -- the last series' peak only @i espresso l:%v -- the espresso series' minimum ``` The `@` letters live in the row slot only, so they need their column selection spelled out: `@x :` addresses every series, and `: @x` is invalid. `@e` and `@a` (median, mean) are reserved — they name a value between cells, not a cell — and an unknown `@` token drops the line, since a failed selector never broadens. #### Stack totals The bare keyword `total` makes the line address **stack sums** instead of cells: one label per x position, over the stacked region the selection touches. `%v` is the sum; every other prop rides unchanged. A column slicer narrows which stacked series contribute, and a row window narrows the x positions: ```novem-labels total l:%v -- the sum over every stack -5: : total l:%v,.0f -- the last five stacks only ``` `total` is line-scoped, like `hide`, and meaningful on stacked types (stacked bars and areas). ### The template The label text is the value of the `l:` directive — one letter that means label text across the whole platform, from axis titles and facet strips to legend entries, and an explicit directive keeps live editing stable: a half-typed word is never reclassified as label text. The value is unquoted when it has no spaces (`l:%v`), quoted when it does (`l:"NVDA ($%v)"`); quotes are `'` or `"` and must match — an unterminated quote recovers loudly, the text running to the end of the line while you type, and the label stays put. Inside the value, the legend token set, re-scoped to the point: | token | meaning | | ------------------------- | ------------------------------------------------------------------------ | | `%v` | this point's value | | `%vl %vf %vi %vx %ve %va` | value stats over the point's series: last, first, min, max, median, mean | | `%l` | the point's row label (category, slice name, date) | | `%s` | the series name | | `%p` | the point's share of its series total, default `.0%` | Format specs append directly to a token, the table cell-format vocabulary: `%v,.1f`, `%p.1%`, `%l%b-%y` (strftime for dates). A line with no `l:` uses the point's **natural label**: `%l`, the category on arcs, the row/x label on cartesian points. So `-1 :` alone labels the last row of every series with its natural label. Bare text is never a template: an unknown bare token drops the whole line with a diagnostic, and an unknown `k:v` drops just that prop. Two keywords, the facet-strip vocabulary: an unquoted `l:none` (or `l:0`) means **no label** — it replaces any earlier label on those cells — and `l:auto` (or `l:1`) spells out the natural default. A quoted value is always literal, so `l:"none"` labels with the text "none". `|` is a **deck break** inside the value: the label stacks vertically, and a leading `<` `>` `-` aligns each deck within the block: Every template glyph escapes with a backslash: `\|` is a literal pipe, and `\-` `\<` `\>` start a deck with that character rather than aligning it — write `l:"\-5% of total"` to keep the minus sign. Those four are the only escapes; a backslash anywhere else is ordinary text. ```novem-labels l:-%l|-%v,.0f -- centered name over centered value ``` ### Position: le, li, lm Three orthogonal keys place the label; the same keys drive facet and axis labels, so you learn them once. | key | values | meaning | | --------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `le:` | `t b l r`, corners `tl tr bl br`, `c` | the anchor edge or corner on the mark. Default: the value end (top of a positive column, bottom of a negative one). | | `li:` | `in`, `out` | which side of that edge the text sits. Stacked segments and arcs default `in`, everything else `out`. | | `la:` | `data`, `axis` | x attachment target. `data` uses the selected mark; `axis` follows the y rail that claims its series (`y2 s:…` before the primary rail). Y remains at that rail's selected value. | | `lm:` | px or `%` of the mark, pair `lm:x,y` | the gap from the anchor edge. Default `2`; unsigned is exact, while `+`/`-` adjusts the default (`lm:+5` is 7px). | | `lml:` `lmr:` `lmt:` `lmb:` | px | per-side margins on the label box. Unsigned is exact; `+`/`-` adjusts the zero default. The anchored side nudges the label away from its mark, while the free side joins the plot's automatic margin. | | `lmx:` `lmy:` `lma:` | px | side bundles under the same absolute/relative rule; a later single side overrides its half. | Text grows into the mark inside and away from it outside, so there is no separate alignment key. On point marks (line vertices, scatter) `le:` is the compass direction from the point. On arcs (pie and donut) the edge set reads radially: `le:t` anchors at the outer rim, `le:b` at the inner edge, `le:c` at the middle of the annulus (the default), and a corner degrades to its radial component (`tl` reads as `t`). The angular position is always the centroid ray. `li:` and `lm:` keep their meanings against the chosen edge — `li:in` sits inside it, `li:out` beyond it (past the rim for `le:t`), and `lm:` is the radial gap. ```novem-labels l:%v le:tl li:in lm:2 -- inside each rect, top-left l:%v le:c -- dead center l:%v le:t li:out lm:2 -- above the bar (the default) l:%l|%p li:in -- donut: name over share, in the arc l:%l li:out lm:8 lr:ray -- outside the arc, on the radial line -1 : l:%s le:r lm:6 lc:mark -- series names at the line ends ``` The last line is the pseudo-legend: the series name rides the last datapoint, in the series color, and the plot reserves the margin it needs. ### Overflow: lo, lw, lh | value | behavior | | --------------------------- | ------------------------------------------------------- | | `lo:e` `lo:t` `lo:w` `lo:o` | ellipsis, truncate, wrap, overflow | | `lo:h` | hide the label when it does not fit | | `lo:f` | flip to the other side of the edge when it does not fit | The former alternate letter (`o:a`) is now the lane policy `af:a`; see the Autofit section below. The size trio bounds the label box, the CSS clamp reading: a fixed size, a minimum and a maximum. Values are px only, greater than zero, and when a minimum and a maximum conflict, **min beats max**. The letters rhyme with the platform: the table width file and the `%vi`/`%vx` value stats already spell min/max as `i`/`x`. | key | meaning | | ------------- | --------------------------------------------------------------------------------------------- | | `lw:` `lh:` | fixed width / height in px | | `lwi:` `lhi:` | minimum — floors the box: equal-width plates, so line-end pseudo-legends align | | `lwx:` | maximum width — caps the text (wrap or ellipsis per `lo:`) and the box and margin it reserves | | `lhx:` | maximum height — caps the deck count; excess decks drop | Inside a mark the fit box is the mark minus margins; outside it is `lwx:` or unbounded. Rect labels default to `lo:f`, the best-fit behavior; everything else defaults to `lo:o`. The old `w:` (which meant max width) is retired; its meaning lives at `lwx:`. ### Autofit Point labels avoid colliding with each other automatically, the way axis tick labels do: one decision per label lane, measured before anything draws. When labels would overlap, every other one mirrors across its mark (the alternate stagger); when even that cannot fit, the lane thins to the smallest uniform step that does — and the **last** point always keeps its label. `af:` is the lane policy, and it is a bare key: it belongs to the line's selection, not to the label box. | key | values | meaning | | ----- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `af:` | absent, `a`, `0` (`none`, `off`) | absent runs the automatic ladder; `af:a` forces the alternate stagger regardless of fit; `af:0` disables collision handling for the lane | Only `af:0` opts a lane out: box props such as `lo:`, `lr:` or `lwx:` style the labels without touching collision handling. ### Ink and vector plates: lr, lc, lb, lbs, lbd, lbp, ltx | key | values | meaning | | ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `lr:` | degrees −90..90, `ray` | rotation; `ray` aligns arc labels to the centroid ray, flipping to stay readable | | `lc:` | color\[,dark], `ink`, `mark` | text color. `ink` (the default) contrast-flips inside dark marks; `mark` inherits the element's color | | `lb:` | `auto`, `mark`, `0`, color\[,dark] | a legibility plate behind the label; `mark` uses the resolved mark color | | `lbs:` | `flat`, `point`, `notch`, `round`, `cut`; one value or `near,far` | vector shapes for the logical ends of the responsive plate. One value applies to both ends. | | `lbd:` | px or `near,far` | exact horizontal depth of shaped ends. `lbd:5` makes a 5px chevron; omitted stays responsive. | | `lbp:` | px or `x,y` | horizontal and vertical plate padding. Unsigned is exact; `+`/`-` adjusts the themed default (`3,2`). | | `ltx:` | style/size/family, any order | the table text vocabulary: `ltx:b`, `ltx:600,sm,mono`, `ltx:12` | | `hide` | bare keyword | drop the sliced points' labels | The plate is a generated SVG path, not a glyph or emoji: its body and ends grow with the measured text, font, wrapping and padding. `near` faces the mark and `far` faces away, so the silhouette remains correct when a label flips from one side to the other. For centered labels, near falls back to the inline-start side. Text defaults to contrast ink against the plate; an explicit `lc:` still wins. An explicit depth participates in layout: `lbd:5 lbp:4,2` lays out the tip, then 5px of shaped end, then 4px of horizontal padding before the text. The point cannot overlap or clip the first glyph. `la:data` keeps a label attached to its mark and lets `lm:` offset that attachment. `la:axis` keeps the value-derived y but places text at the same x as the corresponding y-axis labels. A claimed series follows its claiming rail, so a Volume badge selected by `y2 s:-1` aligns with y2, not the nearer primary y axis. Ordinary labels default to `data`; an outward near-point endpoint defaults to `axis` for line, area, scatter and bar marks. Side margins still apply after axis placement: omit `lml:`/`lmr:` for exact tick-label alignment. For that Bloomberg endpoint, the point tip lands on the domain line while the text lands in the axis-label lane. The tip stays fixed while padding and content reshape the box around the text. The complete rendered SVG text block is measured and centered before padding is added, so multi-deck labels such as `l:"%v|%s|%l"` receive the same space above, below and on both sides. #### Bloomberg-style endpoint This example turns the final value into a mark-colored arrow label. The pointed near end grows with `ltx:` and `lbp:` and the plot reserves its full vector extent. ### Layers and patches A label occupies a **slot**, its resolved `(e, i)` position. One label per point per slot; different slots coexist: ```novem-labels l:%l le:t li:out -- category above every bar l:%v le:c -- value centered inside the same bars ``` Plain lines **define** labels, and later lines replace their slot (later wins). To patch a previous line instead, lead with the `+` attach marker: the line restates the previous one over a new selector, merging its overrides onto what those points already have. A leading `-` erases. ```novem-labels l:%v + -1 : lc:mark ltx:b -- ...and the last points get bold, series-colored ``` ### Diagnostics An unknown bare token drops its whole line; an unknown `k:v` prop drops just that prop. A token still being typed drops alone too: a key without its colon (`lm` before `lm:6`), an `l:` with no value yet, and an unterminated quote, which runs to the end of the line so the label holds its place while you type inside it. Drops are loud but never fatal: the rest of the file still applies, and every drop lands in the plot's diagnostics with its line number. The [playground](/learn/playground) shows them inline. ## Legend ```nohighlight Example chart with complex legend example here. Suggestion: a map with both a gradient and ordinal colors ``` ### Overview The novem legend is closely tied to the novem color formatting as ideally each legend item responds meaningfully to the corresponding novem color formatting. In the abstract you can think about novem legends as being a logical grouping of text and icons to support a novem plot. This grouping can then be placed somewhere within your plot drawing context as an element, or created as a standalone plot setting the type to `legend`. In essence a novem legend consists of three components, `position` places the legend in your plot. `format` styles each legend category and `layout` controls how each `formatted` item is positioned. ### Structure Legends follows the regular novem API config structure as outlined below. ```nohighlight plot_name => Name ├── config => Configuration options ... │ ├── legend => legend specific settings │ │ ├── type => default │ │ ├── position => placement, outside, inside, left, center │ │ ├── layout => layout, rows, grids, alignment │ │ ├── margin => the css m/p pair, legend-scoped │ │ └── format => format, number, text, icons ... ``` ### Type Currently we only support the default novem legends, but the type endpoint is here to support expansion in the future. At the moment this endpoint is read-only and only here for reference. ### Position The first main decision you need to take about your chart is where to place it. By default our ordinal legends are placed centered below where as our continous legends are place outside the chart area top left. The three available dimensions are: - outside or inside the chart area - left, middle or right of the horizontal dimension - top, center or bottom of the vertical dimension There is also `auto` which tries to place the legend where there is the most available space (this is only really relevant when combined with inside). And there is `none`: no legend at all. Nothing renders, no space is reserved, and the automatic legend a `by` color instruction creates is suppressed too. Your format and layout files are untouched, so removing the `none` restores the legend as it was. #### Fixed Finally there is fixed position expressed in two percentage numbers, x and y. `0,0` means top left corner, where as `-0,-0` means bottom right. `+` and `-` can be combined at will so a `-10,20` will be 10% offset from the right side and 20% offset from the top. **Note:** the offset will be from the closest side of the legend box, so if there is not enough space to fit the box to the left of the anchor point, the anchor will automatically flip to be on the right hand side. Same for top and bottom. ```text Outside top left xxxxxxxxxxxxxx +-------------------------------------------------------+ | | | | | | | | | | | | | | | | | | +-------------------------------------------------------+ Outside top middle xxxxxxxxxxxxxx +-------------------------------------------------------+ | | | | | | | | | | | | | | | | | | +-------------------------------------------------------+ Outside top right xxxxxxxxxxxxxx +-------------------------------------------------------+ | | | | | | | | | | | | | | | | | | +-------------------------------------------------------+ ``` ### Layout Layout controls the inner structure of the legend, this is mostly applicable to ordinal lengeds where you might prefer certain structures to your layout. TODO: - grid - rows - cols - auto We also need to talk about width, number of columns and number of rows. ```nohighlight Things to think about: - align left or right with a maxium widht an overflow - spread out - NxM grids - maybe recylce some flexbox things - manual breakpoints (break after n) ``` ### Margin The margins vocabulary, scoped to the legend block — the same letters you know from the [margins file](/docs/reference/plot/config/margins.md) and the label `lm*` family: ```text mt:12 mx:8 -- m*: space OUTSIDE the block. Moves the legend, and -- an outside legend's reserve grows with it: mb:8 -- on a top legend pushes the plot 8px further down. px:24 py:6 -- p*: space INSIDE — px between entries in a row, -- py between rows; pa: sets both. ``` Sides `ml: mr: mt: mb:` (px), bundles `mx: my: ma:`, gaps `px: py: pa:` (non-negative). The value grammar is the margins file's: the `px` suffix is optional, a value can ride the next token (`mr: 15`), whitespace and commas both separate, and `auto` clears an earlier value. Unsigned margins are exact; explicit `+`/`-` adjusts the zero default. Only `%` stays behind, since the legend never sees the svg box it would be a percentage of. The block places as its margin box, so every position mode — the keyword bands, `inside`, `auto`, fixed percentages — composes without special cases. The layout file's old `gap:` retired here: spacing belongs to the `p*` family. ### Format Finally we have the format of the individual legend items. The legend format is somewhat complicated, but once you learn it you'll have access to a very expressive grammar for formatting and structuring your legends. Each format line is `[selector] [k:v props]`. For the common template-only case, omit `l:` entirely: `%cs %l` and `%cl %l` are valid lines. As soon as a colon appears, use the explicit `l:` directive: unquoted when it has no spaces (`l:%l`), quoted when it does (`l:"%cs %l"`). Quotes are `'` or `"` and must match; an unterminated quote recovers loudly — the value runs to the end of the line while you type, and the entry lives. A line with no `l:` keeps the entry's default template, `%cs %l` — so `1 ltx:b` bolds a legend entry while keeping its default text. An unquoted `l:none` (or `l:0`) means **no entry text**, and `l:auto` (or `l:1`) spells out the default; a quoted value is always literal, so `l:"none"` is the text "none". On a property-bearing line, unknown bare text drops the whole line, and an unknown `k:v` drops just that prop. One letter means label text across the whole platform — the same `l:` names axis titles, facet strips and datapoint labels — and the explicit directive keeps live editing stable: a half-typed word is never reclassified as entry text. Entry props share the platform's `l*` label-box vocabulary, the same keys that style datapoint labels, facet strips, axis titles and complications: `lc:` overrides the swatch color, `ltx:` styles the entry text, and `hide` stays bare. The size trio works here too: `lwx:` caps an entry so long series names ellipsize, and `lwi:` floors the block for tabular alignment (min beats max, as everywhere). The old bare spellings (`c:`, `tx:`) are retired and error with a pointer to their `l*` replacement. #### Category mapping The first thing to know is that format instructions can consist of multiple lines, where each line is mapped against the corresponding index of data in the plot, where the last line will be used if there is more categories in the data than lines in the format endpoint. If there are more lines in the format than categories in the data, the remainder of format instructions will be ignored. Thus adding a single format instruction will apply it to all categories in the data set. Similar to colors it is also possible to address entries explicitly by starting a line with a [selector](/docs/reference/api/slicing.md): the category header (case-insensitive, spaces read as underscores), an index, a range or a comma list. Lines apply in order and the last line to address an entry defines it. A selector-less line claims the next entry not yet addressed, and the final selector-less line extends to every entry still unclaimed. ```novem-color-ix performance l:"%cl %l" -- give the performance series a line legend l:"%cs %l" -- make all other categories a square ``` #### Region and value entries A legend captions your colors, so entries can address everything the [color grammar](/docs/reference/plot/config/colors.md) can paint. Two slicers make a **region entry** — one entry captioning a slice of the data, with its swatch resolved from whatever color those cells actually got. A region has no natural name, so the `l:` value carries literal text instead of `%l`: ```novem-color-ix : l:"%cs %l" -- per-series entries as usual -10: : l:"%cs Recent dip" -- one entry for the highlighted region ``` A leading `by` makes **value entries** — the legend side of coloring by category. A text column expands to one entry per distinct value; a quoted value patches a single entry; a numeric column yields one entry whose value tokens cover the column itself, which together with the `%cg` ramp mark makes the continuous legend: ```novem-color-ix by species l:"%cs %l" -- one entry per species by species "Gentoo" l:"%cs The gentoos" -- rename one value by species "Chinstrap" hide -- drop one value by bill_length l:"%vi %cg5 %vx" -- 40 [ramp] 60, the continuous key by 0 l:"%cs %l" -- one entry per X VALUE (col-0 rule) ``` Column 0 — the x column — keys value entries like any other column (`by 0`, or its header name): one entry per distinct x value, the group stats covering that row group's series cells. #### Format instructions Inside the `l:` value, the primary formatting instructions are indicated by `%c` which means the element should be colored and displayed according to the following legend format instruction. There are currently 5 marks or symbols available for legend display: - `%cs` - Square - `%cr` - Rectangle - `%cl` - Line - `%cc` - Circle - `%cg` - Gradient You can also suffx the label with a number indicating the relative width of the label. If you want the line to be a little longer you can use `%cl1.2` or if you want a lot longer gradient you can do `%cg5` In addition you have `%v` which represents the value of that category along with `%l` representing the label header. The standalone `%v` represents the latest value in the given column for which the label represents, but it can be suffixed by the following: - `%vl` - last - `%vf` - first - `%vi` - minimum - `%vx` - maximum - `%ve` - median - `%va` - average (mean) A value token can carry a format spec — the table cell-format vocabulary, appended directly to the token: `%v,.1%` renders the last value as a percentage with one decimal, `%vx,.2m` the maximum as money in millions. Alignment belongs to columns, not tokens. A `|` splits the template into columns laid out side by side, and a leading `<`, `>` or `-` on a column sets its alignment (left, right, middle; left is the default). Column widths are computed across all entries, so values align tabularly. ### Examples ```novem-color-ix l:"%cr|>%v,.1%|<%l (%vx,.2m)" ``` ```nohighlight [ ] 15.1% Brazilian Real (225M) [ ] 5.4% Polish Zloty (25M) [ ] 34.2% United States Dollar (589M) [ ] 25.8% European Euro (417M) [ ] 8.9% Japanese Yen (72M) ``` ```novem-color-ix l:">%v,.1%|%cr|<%l (%vx,.2m)" ``` ```nohighlight 15.1% [ ] Brazilian Real (225M) 5.4% [ ] Polish Zloty (25M) 34.2% [ ] United States Dollar (589M) 25.8% [ ] European Euro (417M) 8.9% [ ] Japanese Yen (72M) ``` ```novem-color-ix exposure l:">%vi,.1m|%cg5|<%vx,.1m %l" l:"%cr %l" ``` ```nohighlight 1.4M [*******] 2.8M Exposure [ ] Above risk threshold [ ] Not applicable ``` ## Margins > **Note:** the margins file is part of the early-access flat config > tree. You can explore it interactively in the > [plot playground](/learn/playground). Margins live in one file directly under your plot's config: `plot` `config/margins` By default every side is **auto**: novem measures your axis labels, ticks and titles and computes the margin that fits them. An unsigned value replaces that calculation exactly; a value prefixed with `+` or `-` adjusts it. Exact margins are useful when charts must align across a grid or document, and they double as the wrap budget for wrapped category labels. The keys are the `m*` family — the same prefix the axis grammar uses for its offsets: `m*` moves things, `p*` pads scales, `e:` picks edges. ### The plot-area margins ```novem-margin ml:15px mr:45, mt:22 ml:60 -- later directives win ml:5% mb:auto -- % of the svg dimension ma:+5 -- add 5px to every auto margin mb:-5 -- subtract 5px from auto bottom ``` - `ml:` `mr:` `mt:` `mb:` are the left, right, top and bottom margins of the plot area. - `mx:` `my:` `ma:` are side **bundles**, the same x/y reading the table's border and padding files use: `mx:` sets left+right, `my:` top+bottom, `ma:` all four. They are pure expansions — a later single-side directive overrides its half (`mx:20 ml:60` leaves right at 20). - An **unsigned** pixel (`px` suffix optional) or `%` value is exact. Prefix it with `+` or `-` to adjust the measured value instead: `ma:+5` adds 5px to all sides, while `mb:-5` subtracts 5px from the calculated bottom. Signed percentages adjust by that percentage of the corresponding svg dimension. - Whitespace and commas both separate directives; several fit on one line; the **last** directive for a side wins; the value may be attached (`ml:15`) or the next token (`ml: 15`). - `auto` restores the measured autocompute — a later `auto` clears an earlier explicit value. - `--` starts a comment, the same marker as every novem grammar. ### The facet gutters When a plot is split into panels with facets, `px:` and `py:` set the gutters between them. Gutters are **inner** spacing, so they take the padding letter, and they read the same way as every other x/y in the platform: `px:` is the gap you cross moving horizontally (between each **column** of panels), `py:` the gap between each **row**: ```novem-margin px:12 py:8 ``` `pa:` bundles both gutters, like `ma:` does for the sides. The gutter is by definition the leftover space after each facet carves out its label, axis and chart area. The side directives describe the **outer frame** of the whole surface; between panels there is no margin, only the gutter — a panel's exterior sides inherit the frame, its interior sides get the gutter, so a stacked price/volume layout comes out right with no configuration at all. Negative gutters pull panels together (overlap allowed); `auto` restores the theme default. Margin and gutter changes animate like any other layout change. ### Per-panel overrides A line leading with the shared slicer pair selects panels by their **physical grid position** — rows × columns, one slicer meaning columns, negatives counting from the end — and overrides those panels' margin boxes: ```novem-margin 0 : mt:40 -- the first grid ROW : -1 mr:0 -- the last grid COLUMN -1 mb:20 -- one slicer = columns ``` Later lines win per side. Sides you don't override follow the exterior/interior rule; `auto` on a scoped line removes that override again. ### Diagnostics Invalid directives are dropped, never fatal: the rest of the file still applies, and every dropped directive lands in the plot's diagnostics with its line number. The [playground](/learn/playground) shows them inline. ## Charts Charts turn quantitative values into geometric shapes. They share a common grid — axes, legend and [colour system](/docs/reference/plot/config/colors.md) — so the same configuration carries across chart types, and novem animates the transition when you switch between them. Start with the [bar chart](/docs/reference/plot/charts/bar.md); for anything bespoke, [custom charts](/docs/reference/plot/charts/custom.md) let you render with your own JavaScript. More chart types are on the way. ## Bar *Embedded visualisation:* `/u/novem_demo/p/en_letter_frequency` ### Overview Novem offers 16 types of barcharts but they can be categorized into three main archetypes: - `bar` - regular barchart - `sbar` - stacked barchart - `gbar` - grouped barchart In addition to the above there is a special aggregate barchart, `abar`, which will be explained in detail [below](#aggregate "About Us"). To accommodate horizontal as well as top/bottom anchoring novem further expands the barcharts with two subcategories, horizontal and inverted. By prepending one of the above type with either `h`, `i` or both you can tell the bars to be anchored to the left side `h`, or right side `ih`. In addition to anchor a bar to the top (making it "grow" down) just append `i` to the regular type. e.g. `ibar` Below is a comprehensive list of possible barchart types. ``` /vis/plots//type Regular Horizontal Inverted Inverted Horizontal ------- ---------- -------- ------------------- bar hbar ibar ihbar sbar hsbar isbar ihsbar gbar hgbar igbar ihgbar abar habar iabar ihabar ``` #### Structure Barcharts follow a regular novem chart structure as outlined below. For a more detailed introduction to the chart structure please see the chart structure details page. ```bash en_letter_frequency => Name ├── config => Configuration options │ ├── caption => Caption below chart │ ├── title => Title of chart │ └── type => bar, sbar, gbar etc... ├── data => data to chart ├── description => Description (meta) ├── name => Name (meta) └── shared => Sharing information ├── +org~group => Shared with an org group ├── @username~group => Shared with a user group └── public => Shared with everyone ``` #### Data ```bash curl -X POST https://api.novem.io/v1/vis/plots//data \ -H "Authorization: Bearer " \ -H "Content-Type: text/plain" \ --data-binary "@data.csv" ``` Like most novem charts the primary way to update the barchart is by writing the information you want visualised to the `/data` endpoint. Novem will assume that the first column is the data for the category axis and the second column the data for the value axis. By default the category axis would be the x-axis, but this will be flipped in the example of `h*` and `ih*` versions. #### Config ### Barchart Types As mentioned in the introduction there are 3 main types of barcharts available on the novem platform. #### Regular *Embedded visualisation:* `/u/novem_demo/p/en_letter_frequency` #### Stacked *Embedded visualisation:* `/u/novem_demo/p/unemployment_nordics` #### Grouped *Embedded visualisation:* `/u/novem_demo/p/state_pop` #### Aggregate *Embedded visualisation:* `/u/novem_demo/p/en_letter_frequency` Aggregate plots are identical to regular plots, except they represent the sum of the columns for each category label. ### Outputs The novem barchart offers six outputs, four for the final visualisation and two for the input data. They can all be found under the `/files` endpoint or by appending the extensions below. For details on the respective output formats please see the [API file documentation](/docs/reference/api/files.md#formats). - `pdf` - a pdf representation `/config/render/orientation` orientation of the plot. - `png` - a png representation `/config/render/orientation` orientation of the plot. - `txt` - a utf-8 textual representation of the plot - `ansi` - a ansi escape sequence colored utf-8 textual representation of the plot - `csv` - a csv file containing the input data - `xlsx` - an xlsx file containing the input data ### Examples This section contains a few practical examples on how to use the API and common use cases. For more comprehensive examples we suggest visiting some of our example charts or reading a couple of our getting started with novem blog posts. #### Create your first barchart *Embedded visualisation:* `/u/novem_demo/p/en_letter_frequency` Welcome to your first barchart, we'll assume that you have a csv file called `en_letter_freq.csv` file two columns: - Character - Frequency If you want to cheat you can find an example file here : `url to en_letter_freq.csv`. ## Custom *Embedded visualisation:* `/u/novem_demo/p/en_letter_frequency` ### Overview ### CSS Novem custom plots run in a sandboxed iframe so that the plot author can use any JavaScript library they like without affecting the surrounding document. The iframe still inherits the parent document's theme however, with every `--novem-*` value propagated in two ways: as CSS variables on the iframe's root, and as a JavaScript object available to the plot's render function. ### Through CSS Variables are injected into the iframe's `:root` before your stylesheet runs, so you can use them directly anywhere you'd write a colour or a font. ```css body { font-family: var(--novem-font-body); font-size: var(--novem-font-size); color: var(--novem-text); background: var(--novem-bg); } .tooltip { background: var(--novem-tooltip-bg); color: var(--novem-tooltip-text); border-radius: var(--novem-tooltip-radius); } ``` ### Through JavaScript Every variable is also exposed on `render.theme` as a camelCase value, which is useful when you build SVG or canvas content from JavaScript. The categorical palette is an array indexed from zero. ```js const t = render.theme; const svg = d3.select(node).append("svg") .attr("width", width) .attr("height", height) .style("font-family", t.fontBody) .style("background", "transparent"); const color = d3.scaleOrdinal(t.colors); // 10 categorical entries svg.append("text") .attr("fill", t.text) .attr("font-weight", t.fontWeightHeading) .text("My chart"); const arc = d3.arc() .innerRadius(parseFloat(t.pieInnerRadius) * r) .outerRadius(r); ``` `render.theme` is dark-mode aware. The value of `t.text` already reflects whichever mode is active, so you don't need to branch on `render.dark` to pick a foreground colour. The boolean is still there if you want it for finer distinctions. > **Note:** A custom chart script (`custom.js`) is capped at **5,242,880 > characters** (5 MB). Custom plots run in an isolated iframe, so everything > must be inlined — minify your script and trim heavy dependencies to stay under > it. See [Size limits](/docs/reference/api/files.md#size-limits) for what happens > when you exceed a cap and the limits on other resources. ## Tables Tables present data in rows and columns with fine-grained control over borders, alignment, padding, formatting and per-cell colour — well suited to documents and e-mails where layout matters. The [e-mail table](/docs/reference/plot/tables/mtable.md) (`mtable`) is a compact, e-mail-safe table; styling is shared with the rest of the plot config, so see [table configuration](/docs/reference/plot/config/table/overview.md) for the available options. ## E-Mail Table *Embedded visualisation:* `/u/novem_demo/p/example_index_monthly_returns` ### Overview Modern e-mail is a complicated environment to try and create nice looking layouts, and with this in mind we've created a collection of novem tables dedicated to e-mails. - `mtable` - mail table, a plain table with some flexible styling Whilst other novem tables offers a static html output, the email tables are designed specifically to be embedded in e-mails. As such there is less flexibility and more restrictions than with other novem plots. The `mtable` serves as a general utility table for adding to e-mails. The goal is to create a compact but nice looking table to include in your e-mails. An `mtable` can be created similar to other novem tables by using our API to create a new `plot` entity of the `mtable` type. #### Structure The structure and available options for a `mtable` plot. ```bash plot_name => Name ├── config => Configuration options │ ├── colors => │ │ └── colors => │ │ ├── type => │ ├── table => table specific settings │ │ ├── index => control table index │ │ ├── size => size of table xs-lg │ │ └── cell => Format table cells │ │ ├── align => Number alignment │ │ ├── border => control table borders │ │ ├── text => Font information (weight, style, color) │ │ ├── format => Number and text format │ │ └── padding => control table format │ ├── render => │ │ └── template => Template to render against │ ├── caption => Caption below chart │ ├── theme => │ ├── title => Title of chart │ └── type => bar, sbar, gbar etc... ├── data => data to chart ├── description => Description (meta) ├── name => Name (meta) └── shared => Sharing information ├── +org~group => Shared with an org group ├── @username~group => Shared with a user group └── public => Shared with everyone ``` #### Data An `mtable` takes the same tabular data as any other novem table. The simplest way to supply it is as CSV written to the `data` endpoint: the first row holds the column headers and the first column is the row index (its header cell is normally left blank). `POST` `/data` ```text ,Date,AUM Example Fund A,2024-07-31,1400000000 Example Fund B,2024-07-31,400000000 ``` The data stays plain numbers and text — number formatting, alignment, borders and colors are all layered on top through the `config/table` options described below. #### Config Mail tables have a few different options, the most relevant being **size** and **colors**. For colors you set the content of your [`plot/config/colors`](/docs/reference/plot/config/colors.md#index-colors) to one of two things: - Either a matrix with index and column matching that of your dataset and the content a valid [novem color value](/docs/reference/theme/colors.md#theme-colors). - Or a list of indices and colors The matrix approach allows you to color code individual cells, whilst the list approach allows you to highlight individual rows. As with other novem colors you can use explicit color codes or one of the predefined "theme" values such as, `ok`, `warn`, `info`, `error`, `bg`. ### Examples Below is a collection of common use cases for the mail table along with example code. #### Create your first mailtable The minimum is a plot of type `mtable` with some data. Set the type: `ctrl` `/config/type` ```text mtable ``` and write a [data](#data) file: `POST` `/data` ```text ,Date,AUM Example Fund A,2024-07-31,1400000000 Example Fund B,2024-07-31,400000000 ``` The table is now available at `/u//p/` and can be dropped into an e-mail with a [`{{ vis }}` section](/docs/reference/markdown/visualisations.md): ```yaml {{ vis ref: /u//p/ }} ``` The examples below layer title, caption, colors and cell formatting on top of this baseline. #### Create a simple heatmap *Embedded visualisation:* `/u/novem_demo/p/example_index_monthly_returns` `ctrl` `/config/type` ```text mtable ``` `ctrl` `/config/title` ```text Monthly performance by sector ``` `ctrl` `/config/caption` ```text The table above shows the year to date, market cap weighted monthly sector returns, for the Novem Example Index. For reference, the total index return is included as well. Information from Yahoo Finance and Nasdaq, calculations by novem, as of 10^th^ of August 2022. **\*** This report is calculated mid-month and as such august is only until 10^th^. **†** The **Novem Example Index** is provided for reference only. **Note:** These numbers are provided as an example of the novem platform only and should not be relied on for any decision making. ``` `ctrl` `/config/colors` ```novem-color-ix 1:-1 : bg lin bad-b,neutral,good-b(_,0,_) -- color from 2nd to 2nd to last row -1 : bg lin bad-b,neutral,good-b(_,0,_) -- color last row ``` `ctrl` `/config/table/cell/border` ```novem-color-border 0 : b 1 inverse -- add a line below the first row (header) -1 : y 1 inverse -- add a line on top of the last row 5 4:6 t 2 red-600 -- Draw red square 5:7 4 l 2 red-600 5:7 6 r 2 red-600 7 4:6 b 2 red-600 : 0 r 1 inverse -- Add vertical lines at beginning : -1 l 1 inverse -- and end ``` `ctrl` `/config/table/cell/padding` ```novem-color-border : : x 1 -- add size 2 padding on left and right side -1 : t 1 -- add size 1 padding on the top 0 : t 2 -- add size 2 padding on the top ``` `ctrl` `/config/table/cell/align` ```novem-color-border : 0 < -- align the left column left 0 1: - -- center the rest of the top row 1: 1: > -- align everything else to the right ``` `ctrl` `/config/table/cell/text` ```novem-color-border 0 : b -- bolden the first row -1 : b -- bolden the last row ``` `ctrl` `/config/table/cell/format` ```novem-color-format 1:-1 1: ,.1% -- format everything except -- first row and first column as pct -1 1: ,.1% -- format last row as pct (skip first column) ``` ## Vars Vars are small data snippets you can attach to any novem plot. Each var has a raw value, a format, a type and a description — novem takes care of formatting and rendering them for you. A single plot can hold many vars, each identified by a short id. Vars are great for surfacing the most important numbers alongside a visualisation: a top performer, a date, a return, a record high. ### Structure ```bash plot_name => Name ├── data => Plot data ├── config => Configuration options ... ├── vars => Plot variables │ └── :var_id => A single variable │ ├── value => Raw value (max 50 characters) │ ├── about => Description (max 250 characters) │ ├── format => Format string │ ├── type => Type indicator │ └── threshold => Threshold value (relative type only) ... ``` Each var lives under `vars/:var_id` where `:var_id` is a short identifier consisting of lowercase letters, digits, dashes and underscores (max 50 characters). For example `top-ret`, `date`, or `hydro_pct`. ### Quick start Creating a var is a two step process: first create it with a `PUT`, then write its fields with `POST`. ```bash # Create the var curl -X PUT .../vis/plots/my-plot/vars/top-ret # Write fields curl -X POST .../vis/plots/my-plot/vars/top-ret/value -d "0.0589" curl -X POST .../vis/plots/my-plot/vars/top-ret/about -d "Return of top performer" curl -X POST .../vis/plots/my-plot/vars/top-ret/format -d "+,.1%" curl -X POST .../vis/plots/my-plot/vars/top-ret/type -d "relative" ``` Once the value is written novem will format it and store the computed result. The formatted text is available at the uservis read endpoint: ```bash curl .../users/trt/vis/plots/my-plot/vars/top-ret # => ↑ +5.9% ``` ### Fields #### value The raw value of the variable as a plain string (max 50 characters). This is the value that gets formatted and colored. `ctrl` `/vis/plots/:plot/vars/:var/value` ```text 0.0589 ``` The value can be a number, a date string, a text label — whatever makes sense for your use case. #### about A short human-readable description of what the variable represents (max 250 characters). `ctrl` `/vis/plots/:plot/vars/:var/about` ```text Return of the top performing S&P 500 stock ``` #### format The format string controls how the raw value is displayed. Novem supports several format families: ##### Text Use `st` to display the raw value as-is, without any formatting. Good for labels, names, tickers. `ctrl` `/vis/plots/:plot/vars/:var/format` ```text st ``` ##### Numbers Novem supports the [python format mini-language](https://docs.python.org/3/library/string.html#format-specification-mini-language) for numeric values. | Format | Value | Output | | ------- | -------- | -------- | | `.1f` | `22.5` | `22.5` | | `,.0f` | `21725` | `21,725` | | `+,.1%` | `0.0589` | `+5.9%` | | `.1%` | `0.2737` | `27.4%` | The `+` prefix adds a sign character for positive values, which is useful for return or change values. ##### Scientific / SI Use the `s` suffix for SI notation. A single integer before the `s` controls precision. | Format | Value | Output | | ------ | --------- | ------- | | `1s` | `1234567` | `1.2M` | | `2s` | `0.001` | `1.00m` | ##### Money Use the `m` suffix for monetary values. An optional currency symbol can be placed before or after the precision. | Format | Value | Output | | ------ | -------- | ---------- | | `m` | `1234.5` | `1,234.5` | | `.2m` | `1234.5` | `1,234.50` | | `$2m` | `446.74` | `$446.74` | | `.2m$` | `446.74` | `446.74$` | ##### Dates Date values are formatted using [strftime directives](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior). The value should be a parseable date string. | Format | Value | Output | | ----------- | ------------ | -------------- | | `%Y-%m-%d` | `2025-12-03` | `2025-12-03` | | `%b %d, %Y` | `2025-12-03` | `Dec 03, 2025` | | `%b %d` | `2025-07-20` | `Jul 20` | #### type The type controls the directional indicator prepended to the formatted value. This is most useful for change or return values where you want a visual cue for positive vs. negative. | Type | Positive | Negative | Neutral | | ------------ | -------- | -------- | ------- | | `relative` | ↑ | ↓ | → | | `triangle` | ▲ | ▼ | ◆ | | `plus_minus` | + | - | = | The indicator is chosen based on the numeric sign of the raw value. For static values (prices, weights, temperatures, counts) use a non-indicator type like `number`. For labels use `text`, for dates use `date`. Any type not listed above simply means no indicator is prepended. **Example:** a var with value `0.041`, format `+,.1%` and type `relative` renders as `↑ +4.1%`. The same var with type `number` renders as `+4.1%` without the indicator. #### threshold The threshold field defines the reference point for the variable's value. This field is only available when the var's type is `relative`. `ctrl` `/vis/plots/:plot/vars/:var/threshold` ```text 0 ``` The threshold is used by the rendering layer to determine how to scale and color the value relative to this reference point. The default threshold is `0`. ### Reading vars #### Listing To list all vars on a plot, read the `vars/` directory: ```bash # Own plots GET /v1/vis/plots/:plot/vars/ # Other users' public/shared plots GET /v1/users/:user/vis/plots/:plot/vars/ ``` #### Reading a single var The uservis endpoint returns the formatted `value_text`: ```bash GET /v1/users/:user/vis/plots/:plot/vars/:var # => ↑ +5.9% ``` To read individual fields on your own plots: ```bash GET /v1/vis/plots/:plot/vars/:var/value GET /v1/vis/plots/:plot/vars/:var/about GET /v1/vis/plots/:plot/vars/:var/format GET /v1/vis/plots/:plot/vars/:var/type GET /v1/vis/plots/:plot/vars/:var/threshold ``` ### Deleting a var ```bash DELETE /v1/vis/plots/:plot/vars/:var ``` This removes the var and all its history. ### History Every time the `value` field is updated, novem takes a snapshot of the var's computed metadata and appends it to a history log. This lets you track how a var changes over time — useful for daily returns, rolling metrics, or any value that gets updated periodically. History is automatic; no extra configuration is needed. Writing to fields other than `value` (like `about` or `format`) does *not* create a history entry. ### Examples Below are a few examples showing common var patterns. #### Returns and changes For values that represent a directional change, use the `relative` type with a percentage format: | var id | value | format | type | output | | --------- | --------- | ------- | ---------- | ---------- | | `dtd-ret` | `0.041` | `+,.1%` | `relative` | `↑ +4.1%` | | `ytd-ret` | `0.178` | `+,.1%` | `relative` | `↑ +17.8%` | | `bot-ret` | `-0.1005` | `+,.1%` | `relative` | `↓ -10.1%` | #### Static numbers For values that are measurements rather than changes, use `number` to omit the indicator: | var id | value | format | type | output | | ---------- | -------- | ------ | -------- | --------- | | `close` | `446.74` | `$2m` | `number` | `$446.74` | | `max-temp` | `31.0` | `.1f` | `number` | `31.0` | | `peak-gen` | `22.5` | `.1f` | `number` | `22.5` | #### Labels and dates | var id | value | format | type | output | | ------------ | ------------------------ | ----------- | ------ | ------------------------ | | `ticker` | `TSLA` | `st` | `text` | `TSLA` | | `top-sector` | `Information Technology` | `st` | `text` | `Information Technology` | | `date` | `2025-12-03` | `%b %d, %Y` | `date` | `Dec 03, 2025` | ## Content ### Content file The content file `/v1/vis/docs//content` expects information to be supplied in [novem markdown](/docs/reference/markdown/overview.md) with document-specific extensions. For the endpoints surrounding the content file (config, assets, sharing, and output formats) see the [document API](/docs/reference/doc/api.md). ### Frontmatter Documents support YAML frontmatter at the beginning of the content file. Frontmatter is enclosed between `---` delimiters and parsed as page 0 metadata. ```yaml --- title: Quarterly Performance Report author: Analytics Team date: January 2025 theme: novem --- ``` Any valid YAML key-value pairs can be used. Common fields include `title`, `author`, `date`, `subtitle` and `theme`. ### Standard markdown Novem documents support standard markdown including headings, paragraphs, bold, italic, lists, code blocks, blockquotes, tables and thematic breaks. In addition, novem extends standard markdown with: - `_underline_` — underlines text (note: `_` is underline, not italic) - `*italic*` — only `*` produces italic - `**bold**` — standard bold - `~~strikethrough~~` — strikethrough - `^superscript^` — superscript - `~subscript~` — subscript (single `~`, not `~~`) - `` `inline code` `` — inline code ### Pages Documents are split into pages using the `{{ page }}` section. Content before the first `{{ page }}` becomes page 0. Each page break starts a new page with optional layout configuration. ```yaml # Page 0 content {{ page orient: landscape cols: 2 }} # Page 1 content (landscape, 2 columns) {{ page }} # Page 2 content (default layout) ``` See the [pages section](/docs/reference/markdown/page-breaks.md) for full configuration options. ### Heading config Headings can carry layout configuration using a trailing `{key:val}` syntax: ```yaml ## Regional Performance {colspan:all; clr,fg,blue-700} ``` This attaches metadata to the heading without displaying the config in the rendered output. Configuration entries are separated by `;` and support both `key:value` pairs and comma-separated lists. ### Sections Like mails, novem documents use sections to embed visuals and control layout. For a full overview of the section format, see the [sections overview](/docs/reference/markdown/sections.md). Available section types: - [Pages](/docs/reference/markdown/page-breaks.md) — page breaks with layout configuration - [Visualisation](/docs/reference/markdown/visualisations.md) — embed novem plots and grids - [Variables](/docs/reference/markdown/variables.md) — inline live values - [Spans](/docs/reference/markdown/spans.md) — footnotes, references and colour spans - [Paragraph](/docs/reference/markdown/paragraph.md) — styled text blocks - [Callout](/docs/reference/markdown/callout.md) — info, warning and error callout boxes - [HTML](/docs/reference/markdown/html.md) — raw HTML passthrough ## API ### Overview Documents follow the same filesystem conventions as the rest of the novem API: folders, files, HTTP verbs, and the `r`/`w`/`d` permission model. If you haven't already, read the [API overview](/docs/guides/api.md) first. This page covers what's specific to the document resource. Your own documents live under `/v1/vis/docs/`, and documents shared with you are reachable under `/v1/users/:user/vis/docs/`. The structure beneath a document is identical on both: ```bash quarterly_report => Document name ├── assets => Per-document images and fonts │ └── ├── config │ ├── comments => Enable/disable comment threads (true/false) │ ├── custom │ │ └── custom.css => Custom CSS overrides │ ├── theme => Theme name (default: novem) │ ├── title => Reserved │ ├── toc => Reserved │ └── type => Layout: doc, pres or blog ├── content => Document content (novem markdown) ├── description => Description (meta) ├── files => Pre-rendered outputs │ ├── doc.pdf │ └── doc.pptx ├── log => Processing/render log (read-only) ├── name => Display name (meta) ├── notifications => Your notification level for this doc ├── shared => Sharing entries ├── shortname => Unique shortname (read-only) ├── summary => Short text summary (meta) ├── tags => Tags on this document ├── threads => Comment threads ├── url => Canonical url (read-only) └── vars => Document variables ``` ### Managing documents | Verb | Path | Description | | -------- | ------------------- | ------------------------------------------- | | `GET` | `/v1/vis/docs` | List your documents | | `PUT` | `/v1/vis/docs/:doc` | Create a new document | | `GET` | `/v1/vis/docs/:doc` | List the document folder | | `PATCH` | `/v1/vis/docs/:doc` | Rename — or copy — the document (see below) | | `DELETE` | `/v1/vis/docs/:doc` | Delete the document | Document names must be lowercase alphanumeric with optional underscores, like all novem resource names. #### Rename and copy `PATCH` accepts either a plain-text body containing the new name, or a JSON body that can additionally request a copy: ```bash # rename curl -X PATCH https://api.novem.io/v1/vis/docs/quarterly_report \ -H "Authorization: Bearer $NOVEM_TOKEN" \ -d "annual_report" # copy instead of rename curl -X PATCH https://api.novem.io/v1/vis/docs/quarterly_report \ -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "report_copy", "copy": true}' ``` A copy clones the content, configuration, and your tags, and gets a fresh shortname. Sharing entries are intentionally **not** copied. The new document starts out private. ### Content and metadata | Path | Access | Description | | ------------- | ------ | --------------------------------------------------------------------- | | `content` | r/w | The document body in [novem markdown](/docs/reference/doc/content.md) | | `name` | r/w | Display name shown in listings and headers | | `description` | r/w | Longer description | | `summary` | r/w | Short text summary | | `shortname` | r | The unique shortname behind the canonical url | | `url` | r | The canonical url of the document | | `log` | r | Processing log — plain text, or JSON with `Accept: application/json` | Writable files take a `POST` with a `text/plain` body; `DELETE` clears them. The `content` body has a generous size cap; see [Size limits](/docs/reference/api/files.md#size-limits). ### Configuration All keys live under `config/` and are written with `POST` (plain text): | Key | Default | Description | | ------------------- | ------- | ------------------------------------------------------------------------------------- | | `type` | `doc` | Layout: `doc`/`docs` (A4 pages), `pres` (16:9 slides), `blog`/`web` (continuous flow) | | `theme` | `novem` | Document theme; org themes are referenced as `+org/theme` | | `comments` | `true` | `true`/`false` — whether comment threads are enabled on the document | | `custom/custom.css` | | Custom CSS applied on top of the theme | | `title` | | Reserved — accepted and stored, but not currently used by the renderer | | `toc` | | Reserved — accepted and stored, but not currently used by the renderer | See the [documents guide](/docs/guides/doc.md) for what each `type` looks like in practice. ### Assets Documents can carry their own images and fonts for use in the content and custom CSS. Assets live under `assets/` and are uploaded with the raw file as the request body: ```bash curl -X POST https://api.novem.io/v1/vis/docs/quarterly_report/assets/logo.png \ -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: image/png" \ --data-binary @logo.png ``` | Verb | Path | Description | | -------- | --------------- | ------------------- | | `GET` | `assets` | List assets | | `POST` | `assets/:asset` | Upload (or replace) | | `GET` | `assets/:asset` | Download | | `DELETE` | `assets/:asset` | Remove | Constraints: - **Content types** — `image/png`, `image/jpeg`, `image/gif`, `image/svg+xml`, `image/webp`, `font/woff`, `font/woff2`, `font/ttf`, `font/otf`. The `Content-Type` header must match. - **Size** — at most 5 MiB per asset. - **Names** — lowercase alphanumeric with `-` separators and an optional extension (`[a-z0-9][a-z0-9-]*(\.[a-z0-9]+)?`), max 64 characters. Assets on a public document are served publicly. Assets on private or shared documents require the same access as the document itself. ### Output formats Pre-rendered outputs are available under `files/`: | File | Description | | ---------- | --------------------------------------------------------------- | | `doc.pdf` | PDF render — A4 pages for `doc`, 16:9 slides for `pres` | | `doc.pptx` | PowerPoint export — each page/slide becomes a slide in the deck | > **Note:** the `files/` listing also shows `doc.png`, `doc.txt` and `doc.ansi` > entries, but these renders are not yet available for documents and currently > return `404`. PDF and PPTX are the supported export formats today. See [files](/docs/reference/api/files.md) for the general story on novem output formats. ### Sharing Documents are private by default. The `shared/` folder controls who can see them: ```bash # share with everyone (requires a public novem profile) curl -X PUT https://api.novem.io/v1/vis/docs/quarterly_report/shared/public \ -H "Authorization: Bearer $NOVEM_TOKEN" # share with one of your groups curl -X PUT "https://api.novem.io/v1/vis/docs/quarterly_report/shared/@username~research" \ -H "Authorization: Bearer $NOVEM_TOKEN" # share with an org group curl -X PUT "https://api.novem.io/v1/vis/docs/quarterly_report/shared/+acme~analytics" \ -H "Authorization: Bearer $NOVEM_TOKEN" ``` `GET shared` lists current entries, `DELETE shared/` revokes access. Sharing with `public` requires your novem profile itself to be public; private accounts cannot publish content to the world. ### Notifications The `notifications` file holds *your* notification preference for this document. It is per-user, not part of the document itself. Valid values are `ignore`, `info`, and `important`; reading it returns the current level (`info` when no override is set). ### Tags, vars and threads - **`tags`** — tag the document with `PUT tags/:tag`, remove with `DELETE`. See [tags](/docs/reference/api/tags.md). - **`vars`** — attach live variables (`value`, `type`, `format`, `about`, `threshold`) that can be referenced from content as `{/u/user/d/doc/v/varname}`. See [vars](/docs/reference/api/vars.md). - **`threads`** — the comment threads attached to the document. `GET threads` lists topics. The `config/options/comments` toggle controls whether commenting is enabled. ### Read-only alias: `/v1/u/:user/d` For quick read access, every document is also reachable through the short [FQNP](/docs/reference/api/terminology.md)-style alias. These endpoints are `GET`-only and respect the same access rules as the full API (public documents need no authentication): | Path | Description | | -------------------------------- | ------------------------------------------------ | | `/v1/u/:user/d` | JSON list of the user's documents visible to you | | `/v1/u/:user/d/:doc` | JSON info envelope for the document | | `/v1/u/:user/d/:doc/pdf` | PDF render | | `/v1/u/:user/d/:doc/pptx` | PowerPoint export | | `/v1/u/:user/d/:doc/v` | JSON list of the document's variables | | `/v1/u/:user/d/:doc/v/:var` | JSON object for one variable | | `/v1/u/:user/d/:doc/v/:var/txt` | Formatted variable value as plain text | | `/v1/u/:user/d/:doc/v/:var/ansi` | Formatted variable value with ansi colors | > **Note:** the alias also routes `img`, `txt`, and `ansi` render paths for > consistency with plots, but (as with `files/` above) those document renders > are not yet available and return `404`. ## Overview E-mails follow the same hierarchical [file structure](/docs/reference/api/files.md) as the rest of the novem API. The canonical path for a mail is `/v1/users/:user/vis/mails/:mail`; for your own mails the shorter `/v1/vis/mails/:mail` alias works everywhere and is what we use below. ### The e-mail itself | Path | Verbs | Description | | --------------------- | -------- | ---------------------------------- | | `/v1/vis/mails` | `GET` | List your e-mails | | `/v1/vis/mails/:mail` | `PUT` | Create a new e-mail with this name | | `/v1/vis/mails/:mail` | `GET` | List the e-mail's folder structure | | `/v1/vis/mails/:mail` | `PATCH` | Rename the e-mail | | `/v1/vis/mails/:mail` | `DELETE` | Delete the e-mail | ### Content and recipients | Path | Verbs | Description | | -------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `.../content` | `GET` `POST` `DELETE` | The e-mail body in [novem markdown](/docs/reference/markdown/overview.md) — see [content](/docs/reference/mail/content.md) | | `.../recipients/to` | `GET` `POST` `DELETE` | Primary recipients — see [recipients](/docs/reference/mail/recipients.md) | | `.../recipients/cc` | `GET` `POST` `DELETE` | Carbon-copy recipients | | `.../recipients/bcc` | `GET` `POST` `DELETE` | Blind-carbon-copy recipients | ### Configuration Every key under `.../config/` is a plain-text file: `GET` reads it, `POST` writes it, `DELETE` resets it to its default. The keys (`subject`, `size`, `theme`, `type`, `reply_to`, `enabled`, `options/comments`) are documented on the [config](/docs/reference/mail/config.md) page. ### Sending and history | Path | Verbs | Description | | ---------------- | ------------ | ---------------------------------------------------------------------------------------------------- | | `.../status` | `GET` `POST` | Trigger a send (`sent` / `test`) and poll its state — see [sending](/docs/reference/mail/sending.md) | | `.../stats/runs` | `GET` | Send history: one row per send with time, duration and status | | `.../log` | `GET` | The e-mail's activity log — recipient changes, send attempts, errors | ### Renders and assets | Path | Verbs | Description | | ------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `.../files/` | `GET` | Rendered outputs: `mail.txt`, `mail.ansi`, `mail.pdf`, `mail.png`, `mail.pptx` — see [renders](/docs/reference/mail/renders.md) | | `.../assets/` | `GET` | List uploaded assets | | `.../assets/:asset` | `GET` `POST` `DELETE` | Upload, fetch or remove an asset (images and fonts, max 5 MiB each, 25 per e-mail) | Assets are referenced from the content with the [img section](/docs/reference/markdown/img.md). Uploads accept `image/png`, `image/jpeg`, `image/gif`, `image/svg+xml`, `image/webp` and the `font/woff`, `font/woff2`, `font/ttf`, `font/otf` font types. Asset names are lowercase alphanumeric with `-` separators and an optional extension (`logo.png`, `brand-font.woff2`), at most 64 characters. ### Metadata | Path | Verbs | Description | | ----------------- | --------------------- | -------------------------------------------------------------------- | | `.../name` | `GET` `POST` `DELETE` | Display name | | `.../description` | `GET` `POST` `DELETE` | Longer description | | `.../summary` | `GET` `POST` `DELETE` | Short summary | | `.../shortname` | `GET` | The auto-generated shortname (read-only) | | `.../url` | `GET` | The e-mail's web url (read-only) | | `.../json/` | `GET` | Read-only system files: `config.json`, `data.json`, `meta_data.json` | ### Shared platform features These work the same on e-mails as on every other novem visual: | Path | Description | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `.../shared/` | Who can *view* the e-mail on the web — add `public`, `+org~group` or `@user~group` entries with `PUT`, list with `GET`, revoke with `DELETE` | | `.../tags/` | Tagging — see [tags](/docs/reference/api/tags.md) | | `.../vars/` | Attach metadata variables — see [vars](/docs/reference/api/vars.md) | | `.../threads/` | Comment threads on the e-mail's web page (toggle with `config/options/comments`) | | `.../notifications` | Your personal notification level for this e-mail: `ignore`, `info` or `important` | > **Note:** entries in `shared/` control who can view the e-mail render on the > web. They are unrelated to who *receives* the e-mail. Recipients live under > `recipients/`. ### Public short links Each e-mail is also reachable at `/v1/u/:user/m/:mail`, a compact, read-only view of the rendered output that honours the e-mail's sharing settings. See [renders](/docs/reference/mail/renders.md#short-links) for the full list of formats. ## Content ### Structure E-mails (or mails for short), like most other novem visuals follows the same hierarchical folder structure that you're used to seeing. Mails are mostly similar to Documents in that it has a primary file `content` where most of the information for the mail is stored. In addition to the content file, there are also supporting files and folders containing information such as recipients, attachments, configuration etc. Below is an illustrative example of an overall e-mail structure. ```bash daily_email_summary => E-mail ID ... ├── content => Content of e-mail ... ``` #### Content file The content file `/v1/vis/mails//content` expects information to be supplied in [novem markdown](/docs/reference/markdown/overview.md). ### Sections Like documents, novem e-mails use the novem markdown section extensions. Sections can be used to embed visuals and control layout in novem mail. For a full overview of the section format and common parameters, see the [sections overview](/docs/reference/markdown/sections.md). Available section types: - [Preview](/docs/reference/markdown/preview.md) — preview text for inbox list views - [Paragraph](/docs/reference/markdown/paragraph.md) — styled text blocks with font size, style, alignment - [Visualisation](/docs/reference/markdown/visualisations.md) — embed novem plots and grids - [Callout](/docs/reference/markdown/callout.md) — info, warning, error and success callout boxes - [Author](/docs/reference/markdown/author.md) — author information blocks #### Preview text The preview section provides a short preview text that will only be visible in the list view of e-mail clients. It will not render in the e-mail body. ```yaml {{ preview }} A short preview text that will show up in the list view. {{ /preview }} ``` #### Embedding visualisations Novem plots and grids can be embedded directly in your e-mail using the vis section. ```yaml {{ vis ref: /u/novem_demo/p/state_pop width: 100% align: center include title: false include caption: true }} ``` See the [visualisation section](/docs/reference/markdown/visualisations.md) for full details on reference formats and options. #### Embedding images Static images (logos, diagrams, photos) are embedded with the [img section](/docs/reference/markdown/img.md). The image itself is uploaded as an **asset** on the mail first: ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ -H "Content-Type: image/png" \ --data-binary @logo.png \ https://api.novem.io/v1/vis/mails/daily_email_summary/assets/logo.png ``` and then referenced by name in the content: ```yaml {{ img ref: logo.png }} ``` Assets accept the common image types (`png`, `jpeg`, `gif`, `svg`, `webp`) and web fonts, up to 5 MiB each and 25 assets per e-mail. At send time the image bytes are attached to the e-mail itself, so recipients see them without hot-linking. ### Sending an e-mail Sending an e-mail is fairly straightforward, simply POST `sent` to the `status` endpoint in the mail structure and all [valid recipients](/docs/reference/mail/recipients.md#valid-recipients-and-restrictions) in the recipients folder (to, cc, bcc) will receive an e-mail. For test sends, rate limits and how to follow a send's progress, see [sending](/docs/reference/mail/sending.md). ### Examples #### A daily e-mail of webpage visitors ```yaml {{ preview }} Daily summary of website visitors and key metrics. {{ /preview }} Good morning User, yesterday there were 4,323 unique users who visited your webpage. {{ vis ref: /u/novem_demo/p/example_visits width: 100% align: center include caption: true }} {{ para sz: s fg: gray-600 st: i }} This is an automatically generated e-mail. {{ /para }} ``` ```bash # create a new novem e-mail novem -m mail_name -C # add test@novem.io to the "to" recipient list novem -m mail_name -w recipients/to example@novem.io # write the content of the draft.md to the mail cat draft1.md | novem -m mail_name # send the e-mail novem -m mail_name -w status sent # shorthands cat draft.md | novem -m mail_name --to example@novem.io -S ``` ## Recipients ### Structure E-mails (or mails for short), like most other novem visuals follows the same hierarchical folder structure that you're used to seeing. Who receives the e-mail is controlled by three plain-text files in the `recipients` folder: ```bash daily_email_summary => E-mail ID ... ├── recipients => Who should receive the e-mail │ ├── to => List of primary recipients │ ├── cc => List of cc recipients │ └── bcc => List of bcc recipients ... ``` ### Working with the lists Each of `to`, `cc`, and `bcc` holds a whitespace-separated list of recipients. One per line, by convention: ```bash # replace the "to" list novem -m daily_email_summary -w recipients/to "reports@example.com @novem_demo" # read it back novem -m daily_email_summary -r recipients/to # clear it curl -X DELETE -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/vis/mails/daily_email_summary/recipients/to ``` A few things to know: - A `POST` **replaces the whole list**, not appends to it. Write the full set each time. - Writes are atomic: if *any* line can't be applied (unknown user, over your plan limit, unparsable entry), the request fails with a `400` listing the rejected lines and **nothing changes**. - A `DELETE` (or a `POST` with an empty body) clears the list. - You must have added and verified your own e-mail address before you can write recipients (`424` otherwise). - Every change is recorded in the e-mail's `/log`. ### Recipient formats | Format | Example | Meaning | | -------------- | -------------------------------- | ---------------------------------------------- | | E-mail address | `reports@example.com` | Any external address | | Novem user | `@username` | The user's registered address | | Yourself | `me`, `self` or `@your_username` | Your own registered address — always allowed | | Org group | `+org~group` | Every accepted member of an organisation group | | User group | `@user~group` | Every accepted member of a user's group | | Personal group | `~group` | Shorthand for `@your_username~group` | For a **novem user** to be addable they must exist, have a public profile and a verified e-mail address. For a **group** to be addable you must be a `superuser`, `admin` or `founder` of that group; at send time the e-mail goes to every accepted member with a verified address. > **Note:** an organisation group can also have its own **inbound** address. > Mail sent to it triggers the [jobs](/docs/guides/jobs.md) shared with the group > rather than reaching its members. It is computed and read-only, of the form > `@.`, and exists only when the org's mail > subdomain is enabled and the group accepts inbound mail. Read it from the > group's `email` sub-resource — see > [groups & organisations](/docs/reference/api/groups_and_orgs.md). ### Valid recipients and restrictions How many recipients you can address depends on your subscription. The limits apply per e-mail and per category. External addresses, novem users, and groups count separately: | Plan | External addresses | Novem users | Groups | | ---------- | ------------------ | ----------- | ------ | | Free | 0 | 0 | 200 | | Basic | 5 | 20 | 20 000 | | Premium | 25 | 200 | 20 000 | | Enterprise | 500 | 200 | 20 000 | Regardless of plan you can always send to yourself (`me`), so free accounts can use novem e-mail for their own reports and alerts. Enterprise limits are negotiated on a per-case basis. The limits are enforced both when you write the recipient lists and again when an e-mail is sent. ### Sending Once the recipients are in place, sending is a single write to the e-mail's `status` endpoint. A `test` mode delivers only to your own verified address. See [sending](/docs/reference/mail/sending.md) for the full lifecycle, checks, and rate limits. ### Anti-spam To avoid spamming or the sending of unwanted e-mails, novem always adds an unsubscribe link to the footer of our e-mails. If a recipient unsubscribes from an e-mail then no more e-mails from that particular ID will be sent to that recipient again. If a new e-mail is created with the same recipient and the recipient unsubscribes from that id as well, then no further e-mails from the sending account will be delivered to that recipient unless the recipient chooses to explicitly whitelist the account. There are several other anti-spam techniques as well, but those are the most common errors you should expect to see. ## Config E-mail configuration follows the everything-is-a-file pattern: each option is a small plain-text file under `/v1/vis/mails/:mail/config/`. You write a value with a `POST`, read it back with a `GET`, and a `DELETE` restores the default: `POST` `/v1/vis/mails/weekly_report/config/subject` ```text Weekly report — week 24 ``` ### Available options | Key | Default | Values | Description | | ---------- | ------------- | ------------------------------------ | --------------------------------------------------------------------------- | | `subject` | *(empty)* | free text | The subject line of the e-mail | | `size` | `small` | `xs`, `s`, `m`, `l` (or spelled out) | Content width of the rendered e-mail | | `theme` | `novem-light` | `novem`, `novem-light`, `novem-dark` | Color theme used for the render | | `type` | `simple` | `simple` | The e-mail layout — `simple` is currently the only layout | | `reply_to` | `no` | `yes` / `no` | Set to `yes` to add your registered e-mail address as the `Reply-To` header | | `enabled` | `true` | truthy / falsy | Kill switch: a falsy value blocks production sends | | `comments` | `true` | `true` / `false` | Whether viewers can open comment threads on the e-mail's web page | #### `size` The size controls the rendered content width. Both the short and the spelled out forms are accepted: | Value | Width | | -------------------- | ------- | | `xs` / `extra small` | 400 px | | `s` / `small` | 600 px | | `m` / `medium` | 900 px | | `l` / `large` | 1200 px | An unrecognised value falls back to the default `small`. #### `theme` `novem` and `novem-light` are light renders, `novem-dark` is the dark variant. An unrecognised theme falls back to `novem`. Terminal ([ansi](/docs/reference/mail/renders.md)) renders produce both a light and a dark variant regardless. #### `enabled` The send kill switch. The values `no`, `n`, `nope`, `false`, `f`, and `off` count as falsy. While one of them is set, a production send is rejected with a `403` and a note in the e-mail's `/log`. Test sends (see [sending](/docs/reference/mail/sending.md)) are exempt, so you can keep iterating on a disabled e-mail safely. #### `reply_to` By default, novem e-mails are sent from a no-reply address. Set `reply_to` to exactly `yes` and the `Reply-To` header of outgoing mails points to your registered novem e-mail address, so recipients can answer you directly. #### `options/comments` Controls the comment [threads](/docs/reference/mail/overview.md#shared-platform-features) on the e-mail's web view. With `options/comments` set to `false`, creating new topics and comments is blocked. Existing comments remain visible. > **Note:** writes are plain text: no quotes, no JSON. A trailing newline is > fine and ignored. ### Reading the whole config The read-only file `/v1/vis/mails/:mail/json/config.json` returns the active configuration as a single JSON document, convenient when you want a snapshot instead of one request per key. ## Sending Sending is a single write: `POST` the word `sent` to the e-mail's `status` file and novem queues a delivery to every valid entry in the [recipients](/docs/reference/mail/recipients.md) folder. `POST` `/v1/vis/mails/weekly_report/status` ```text sent ``` A successful trigger answers `202 Accepted`; delivery itself is asynchronous. Only the **owner** of an e-mail can send it. ### Test sends Write `test` instead and the rendered e-mail goes to exactly one address: your own registered and verified one. Recipient lists, the `config/enabled` switch and the send rate limit are all bypassed, which makes `test` the safe way to iterate on content: **CLI** ```bash # send a test of the current draft to yourself novem -m weekly_report -w status test # happy? send it for real novem -m weekly_report -w status sent ``` **API** ```bash curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \ --data "test" \ https://api.novem.io/v1/vis/mails/weekly_report/status ``` > **Note:** with the bare-word body, anything that does not start with `test` > is treated as a production send request. For finer control, including ad-hoc > recipients, use the JSON body described next. ### Ad-hoc recipients The `status` file also accepts a JSON body. This generalises the trigger and lets you supply the recipients inline, instead of maintaining the [recipients](/docs/reference/mail/recipients.md) folder ahead of time: `POST` `/v1/vis/mails/weekly_report/status` ```json { "action": "send", "to": ["alex@example.com", "sam@example.com"] } ``` The `to` addresses **replace** the e-mail's stored recipient list for that one send; the stored list itself is left untouched. Only the `to` vector is supported: ad-hoc sends take direct addresses, not `cc`, `bcc`, `@user` handles or groups. The content, subject and theme still come from the e-mail itself, so an ad-hoc send changes only who receives this particular delivery. `action` selects the send type and defaults to `send`. `{ "action": "test" }` is the JSON equivalent of the bare `test` word, and `{ "action": "send" }` with no `to` array behaves exactly like the bare `sent` word: a normal send to the stored recipients. Ad-hoc addresses count against your plan's [direct-address allowance](/docs/reference/mail/recipients.md#valid-recipients-and-restrictions) and pass through the same unsubscribe and bounce suppression as stored recipients. A malformed body, an unknown `action`, a non-array `to`, an empty `to`, or an invalid address is rejected with `400`. > **Note:** the in-flight and rate-limit checks below are scoped to the > recipient set for an ad-hoc send. Sending the same e-mail to a different > address in a second call is accepted right away; only an identical re-fire to > the same recipients within the in-flight window is held back. ### Checks before a send is accepted Novem validates a send request before queueing it. The full list, in order: | Check | Failure response | | ----------------------------------------------------- | ------------------------------------------------------------ | | You own the e-mail | `404` | | Your own e-mail address is verified | `403` | | No send of this e-mail is already in flight | `425` — try again once it completes | | At most 10 sends of the *same content* per 30 minutes | `429` with `retry_after_seconds` in the body | | `config/enabled` is not set to a falsy value | `403` — see [config](/docs/reference/mail/config.md#enabled) | The rate limit is content-based: editing the e-mail (or anything it embeds) resets the window, so iterating on a report never runs into it. Only re-sending the identical e-mail does. For [ad-hoc sends](#ad-hoc-recipients) the window also keys on the recipient set, so the same e-mail to different addresses counts independently. Test sends are exempt from both the rate limit and the `enabled` check. Recipient lists are resolved at send time, and your [plan's recipient limits](/docs/reference/mail/recipients.md#valid-recipients-and-restrictions) are applied to the resolved list. ### Polling the status `GET` on the same endpoint tells you where the e-mail is in its lifecycle: | Value | Meaning | | --------- | ------------------------------------------- | | `ready` | No send in progress; the e-mail can be sent | | `sending` | A production send is being processed | | `testing` | A test send is being processed | ### Send history `GET /v1/vis/mails/:mail/stats/runs` returns one row per send with the timestamp, duration in seconds, and final status: plain text by default, JSON with an `Accept: application/json` header. ### The log Everything that happens to an e-mail (recipient changes, send triggers, rejected sends and the reason why) is recorded in its log: ```bash novem -m weekly_report -r log # or curl -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/vis/mails/weekly_report/log ``` When a send doesn't behave the way you expect, the log is the first place to look. ## Renders The same [content](/docs/reference/mail/content.md) that goes out as an e-mail can be fetched in a range of formats. See the [files reference](/docs/reference/api/files.md) for the general philosophy. This page lists what's available for e-mails specifically. ### The files folder Authenticated renders live in the e-mail's `files/` folder: | Path | Format | | ------------------------------------- | ------------------------- | | `/v1/vis/mails/:mail/files/mail.txt` | Plain text | | `/v1/vis/mails/:mail/files/mail.ansi` | 256-color terminal output | | `/v1/vis/mails/:mail/files/mail.pdf` | PDF | | `/v1/vis/mails/:mail/files/mail.png` | PNG image | | `/v1/vis/mails/:mail/files/mail.pptx` | PowerPoint | The `txt` and `ansi` renders accept `cols` and `rows` query parameters to control the terminal geometry, e.g. `mail.ansi?cols=120`. ### Short links Each e-mail is also reachable under the compact `/v1/u/:user/m/:mail` form. These endpoints honour the e-mail's sharing settings (its `shared/` folder): anyone with read access can fetch them, and a publicly shared e-mail needs no authentication at all. | Path | Returns | | ----------------------------------- | ---------------------------------------------- | | `/v1/u/:user/m` | JSON list of the user's e-mails visible to you | | `/v1/u/:user/m/:mail` | JSON metadata for the e-mail | | `/v1/u/:user/m/:mail/txt` | Plain text render | | `/v1/u/:user/m/:mail/ansi` | Terminal (ansi) render | | `/v1/u/:user/m/:mail/txt/html` | The text render wrapped as an HTML page | | `/v1/u/:user/m/:mail/ansi/html` | The ansi render wrapped as an HTML page | | `/v1/u/:user/m/:mail/img` | PNG image render | | `/v1/u/:user/m/:mail/txt/html/img` | PNG image of the text view | | `/v1/u/:user/m/:mail/ansi/html/img` | PNG image of the ansi view | | `/v1/u/:user/m/:mail/pdf` | PDF render | | `/v1/u/:user/m/:mail/pptx` | PowerPoint render | As with the files folder, the `txt` and `ansi` variants accept `cols` and `rows` query parameters. ### Vars An e-mail's [vars](/docs/reference/api/vars.md) get the same public treatment: | Path | Returns | | --------------------------------- | ------------------------------------- | | `/v1/u/:user/m/:mail/v` | JSON list of the e-mail's vars | | `/v1/u/:user/m/:mail/v/:var` | JSON detail for one var | | `/v1/u/:user/m/:mail/v/:var/txt` | The var's formatted plain-text output | | `/v1/u/:user/m/:mail/v/:var/ansi` | The var's formatted terminal output | > **Note:** renders are produced on demand and cached. The first request > after a content change can take a moment. Repeat fetches are fast. ## Layout A grid has exactly two content files, both written as plain text (`Content-Type: text/plain`): - `layout` — the geometry: which panels exist and how big they are. - `mapping` — the content: which plot goes in each panel. Writing either file re-parses both, so they can be updated independently and in any order. The parsed result is available as JSON under the grid's `json/data.json` (structure) and `json/meta_data.json` (mapping refs). ### The layout file The layout is a rectangle drawn with three characters: `+`, `-` and `|`. Boxes are split into smaller boxes by divider lines, and each innermost box holds one instruction naming the plot label to show there: ```text +-------------+----------+ | | | | {A:-} | {B:-} | | | | | | | | | | +-------------+----------+ ``` The rules: - **One outer frame.** The whole drawing is one rectangle; blank rows and columns around it are ignored. - **Dividers split a box in two (or more).** A divider is a full row of `-`/`+` (horizontal split) or a full column of `|`/`+` (vertical split) running edge to edge inside a box. A divider can't mix `-` and `|`. - **A pipe-ended horizontal divider floats.** `|---------|` creates a floating vertical stack. Its ASCII heights remain the default allocation, but a renderer may redistribute unused height between the stack's children. Boundary-connected `+---------+` dividers stay fixed and are resolved first, so floating stacks nest inside the fixed layout. The parsed wrapper is marked with `"floating": true`; fixed wrappers omit the field. - **Splits nest.** Each sub-box can be divided again, alternating directions as needed. Any rectangular arrangement is expressible. - **Size is proportional.** The distance between a box's border characters becomes its share of the rendered dashboard. Shared divider characters have no size of their own. In the example above the left panel spans 14 of 25 horizontal units, so it gets 56% of the width. - **One instruction per box.** Whatever remains in an innermost box after removing drawing characters and whitespace must be a single `{LABEL}` instruction. #### Instructions ```text {LABEL[:POSITION]} ``` - `LABEL` — an alphanumeric label (`A`, `B2`, `perf`, …) defined in the mapping file. A label with no matching mapping line leaves the cell empty. - `POSITION` — an optional vertical anchor for the cell's content: | Position | Meaning | | -------- | ---------------- | | `^` | top | | `-` | middle (default) | | `v` | bottom | `{A}`, `{A:}` and `{A:-}` are equivalent. Any other position character makes the instruction invalid and the cell renders empty. In a fixed cell, position anchors content within its allocation. In a floating stack, it also tells the renderer where the content should stay when sibling heights are redistributed. ### The mapping file The mapping file binds layout labels to plots, one binding per line: ```text LABEL => REFERENCE[,PARAM=VALUE]... ``` For example: ```text A => /u/novem_demo/p/nei_rgn_perf,c=f B => https://novem.io/u/novem_demo/p/nei_rgn_tb,c=t,h=f,m=d,s=2,tbg=f ``` - `LABEL` — the label used in the layout file. - `REFERENCE` — a plot shortname or path. The equivalent forms `/p/my_plot`, `p/my_plot`, `/u/you/p/my_plot`, `u/you/p/my_plot`, and `https://novem.io/u/you/p/my_plot` are all accepted. - Comma options are the preferred form because they are concise and easy to type, for example `/u/you/p/my_plot,m=d,s=2`. - Standard query parameters are also accepted, so a working Novem URL such as `/u/you/p/my_plot?m=d&scale=2` can be pasted directly into a mapping. If both forms set the same option, the later comma option wins. Whitespace anywhere on the line is ignored, and lines without a `=>` are skipped. #### Parameters | Parameter | Alias | Values | Default | Description | | ------------------------ | ----- | ------------------------- | ---------------- | ------------------------------------------ | | `include_header` | `h` | `t`, `true`, `f`, `false` | `false` | Include the plot's header in the cell | | `include_caption` | `c` | `t`, `true`, `f`, `false` | `false` | Include the plot's caption in the cell | | `mode` | `m` | `l`, `light`, `d`, `dark` | inherit | Render this cell in light or dark mode | | `scale` | `s` | `1`, `2`, `3` | consumer default | Set the device or raster scale factor | | `transparent_background` | `tbg` | `t`, `true`, `f`, `false` | consumer default | Request a transparent or filled background | An unrecognised value falls back to the parameter's default. The preferred comma form uses the compact `s=2` alias and stores it as `scale: 2`. Query strings should use `/i`'s canonical `scale=2` spelling because `/i` reserves `s` for legacy image-size codes. These are per-cell editorial controls, like the options on a [`{{ vis }}` section](/docs/reference/markdown/visualisations.md) in a document or e-mail. If `m`, `scale` or `tbg` is omitted, the field is left out of metadata so a TV, renderer or other consumer can inherit its surrounding context. > **Note:** if the same label appears on several mapping lines, the last line > wins. Labels that exist in the mapping but not in the layout are simply > unused. ### Reading the parsed result After a successful write the parsed layout is exposed on the grid's JSON endpoints, useful for debugging a layout that doesn't split the way you expected: ```bash curl -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/vis/grids/my_dashboard/json/data.json ``` ```json { "structure": { "type": "wrapper", "width": 25, "height": 6, "children": [ { "type": "wrapper container", "width": 14, "height": 6, "children": [], "inst": { "lid": "NL--hwckacar", "position": "middle", "include_header": false, "include_caption": false } }, { "type": "wrapper container", "width": 11, "height": 6, "children": [], "inst": { "lid": "NL--tguxadkh", "position": "middle", "include_header": false, "include_caption": true } } ] } } ``` The `lid` values are internal link ids connecting each cell to its mapping entry. `json/meta_data.json` lists them alongside the labels and normalized per-cell presentation options: ```bash curl -H "Authorization: Bearer $NOVEM_TOKEN" \ https://api.novem.io/v1/vis/grids/my_dashboard/json/meta_data.json ``` ```json { "refs": [ { "label": "B", "lid": "NL--tguxadkh", "include_header": false, "include_caption": true, "mode": "dark", "scale": 2, "transparent_background": false } ] } ``` ### See also - [Grid API](/docs/reference/grid/api.md) — every grid endpoint. - [API overview](/docs/guides/api.md) — verbs, permissions and the filesystem metaphor. ## API Grids follow the same conventions as the rest of the novem API: the filesystem metaphor, the HTTP verbs, plain-text writes, and the `r`/`w`/`d` permission model. See the [API overview](/docs/guides/api.md) for the general mechanics. This page lists what's specific to grids. Every path below also answers `OPTIONS` with the verbs valid for your token. ### Addressing a grid A grid lives at three equivalent addresses: | Path | Use | | --------------------------------- | ------------------------------------------------------------------------------------- | | `/v1/vis/grids/:grid` | Your own grids (shorthand for your username) | | `/v1/users/:user/vis/grids/:grid` | Any user's grids, yours or ones shared with you | | `/v1/u/:user/g/:grid` | Read-only [FQNP](/docs/reference/api/terminology.md) alias with JSON info and renders | The first two serve the same tree. When you address another user's grid, your access is decided by its shares: reads need a share granting `r`, writes `w`, deletes `d`. The examples below use the `/vis/grids` form. ### Lifecycle | Verb | Path | Description | | -------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- | | `GET` | `/vis/grids` | List your grids | | `PUT` | `/vis/grids/:grid` | Create a grid — ids are lowercase alphanumerics plus `-` and `_` (not leading) | | `GET` | `/vis/grids/:grid` | List the grid's files and folders | | `PATCH` | `/vis/grids/:grid` | Rename — body is the new id (`text/plain`), or `{"name": "...", "copy": true}` (`application/json`) to copy instead | | `DELETE` | `/vis/grids/:grid` | Delete the grid | ### Content The two files that define the dashboard itself. See the [layout reference](/docs/reference/grid/layout.md) for their grammar: | Verb | Path | Description | | ------------------------- | -------------------------- | -------------------------------- | | `POST` / `GET` / `DELETE` | `/vis/grids/:grid/layout` | The box drawing of the dashboard | | `POST` / `GET` / `DELETE` | `/vis/grids/:grid/mapping` | Binds layout labels to plots | `DELETE` on a file endpoint truncates it (clears the content) rather than removing the path. Both files are capped. See [Size limits](/docs/reference/api/files.md#size-limits). ### Metadata | Verb | Path | Description | | ------------------------- | -------------------------------- | ------------------------------------------------------------------------------------- | | `POST` / `GET` / `DELETE` | `/vis/grids/:grid/name` | Display name | | `POST` / `GET` / `DELETE` | `/vis/grids/:grid/summary` | One-line summary | | `POST` / `GET` / `DELETE` | `/vis/grids/:grid/description` | Longer description, rendered as markdown | | `GET` | `/vis/grids/:grid/shortname` | The grid's unique short id | | `GET` | `/vis/grids/:grid/url` | Canonical url (`https://novem.io/g/`) | | `GET` | `/vis/grids/:grid/log` | Processing log | | `POST` / `GET` | `/vis/grids/:grid/notifications` | Your notification preference for this grid: `ignore`, `info` (default) or `important` | ### Configuration All config keys are plain-text files supporting `POST` (write), `GET` (read) and `DELETE` (reset to default). `GET` on `config` and its sub-folders lists the keys. | Path | Default | Description | | ------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------ | | `config/type` | `dashboard` | Rendering mode: `dashboard`, `landscape`, `portrait` or `stretch` (aliases like `ls`, `pt`, `fs`, `dash` accepted) | | `config/theme` | `novem` | Color theme | | `config/options/comments` | `true` | Set to `false` to block new comment threads on the grid | | `config/borders/style` | `thin` | Panel border style | | `config/borders/corner` | `square` | Panel corner style | | `config/borders/color` | `* gray-300` | Panel border color | | `config/borders/label` | *(empty)* | Panel border label | | `config/colors/theme` | `#fffff` | Frame color theme | #### Render chrome (premium) The `config/render` folder adds a header and footer band around the dashboard and lets you style them. These endpoints require a **premium** or **enterprise** subscription: | Path | Description | | ------------------------------- | -------------------------------------------------------------------------------------- | | `config/render/header/title` | Dashboard title — falls back to the grid's name when unset | | `config/render/footer/footnote` | Footer text | | `config/render/footer/pagenum` | Page-number setting for paged output | | `config/render/styles` | Custom CSS injected into the render (e.g. `@font-face` rules for the title and footer) | `title` and `footnote` accept a sanitised subset of HTML: `img`, `b`, `strong`, `i`, `em`, `span`, and `br`. Anything else, including `