Current section
Files
Jump to
Current section
Files
README.md
# Fude

Fude (筆, the brush) is charts for Phoenix. Sumi is the ink; Fude draws
with it. It gives your app:
* charts for LiveView from one line of Elixir:
`Fude.bar(sales, x: :region, y: :revenue)`, and the full grammar
(channels, aggregates, units of time, bins, layers) when you need it;
* charts that look edited, not generated: titles, subtitles and sources
as a graphics desk sets them, labels at the ends of lines, hairline
grids, rounded bar ends, notes on the data (`Fude.annotate/3`), small
multiples (`Fude.facet/3`), and the Sumi theme;
* bars, lines, areas, points, bubbles, pies, donuts, histograms,
heatmaps, waterfalls, funnels, sparklines, region maps of the world
and the US states, and single numbers (a number, a trend, a gauge, a
progress bar);
* charts that move: when the data changes, each mark goes from where it
was to where it is; new points stream in with `Fude.Live.append/5`
without sending the data again;
* your data as you have it: a list of maps, a query result, or an Ecto
query that the database groups and aggregates (`Fude.query/3`), so a
chart of fifty million rows reads a few hundred;
* a chart as plain data: `Fude.Spec.dump/1` gives a map with a version
to save in a column or a file, and `Fude.Spec.load/1` takes it back,
with every error where it is and what to write instead;
* tooltips with every series at the pointer, a legend that highlights,
the keyboard (the arrows move through the data, a screen reader says
each value), and a table of the data under each chart;
* SVG and PNG files of a chart, from the same drawing as the screen;
* collaboration on a chart, with [Issho](https://hex.pm/packages/issho)
(`Fude.Issho`): a chart's spec as a room's shared state, the others'
pointers on its marks, and tools for agents that change it as people
do;
* `Fude.Test`, to read what a chart shows in `Phoenix.LiveViewTest`
with no browser.
The charts are a prebuilt JavaScript bundle in the Hex package, 14 KB
gzipped. Your app does not run Node.js for it: you add the hook and one
style sheet.
## What the charts do
| | |
| --- | --- |
| Kinds | `bar` (and `horizontal: true`), `line`, `area`, `scatter`, `bubble`, `pie`, `donut`, `histogram`, `heatmap`, `waterfall`, `funnel`, `sparkline`, `choropleth`, `bubble_map`, `rule`, `text`, `number`, `trend`, `gauge`, `progress`; `Fude.layers/3` for several over each other |
| Channels | `x`, `y`, `color`, `size`, `value`, `region`, `tooltip`: a field, `{:sum, :revenue}`, `{:month, :created_at}`, `{:bin, :total}`, or the full form with a type, a sort, a format, a title, a scale and a stack |
| Data | lists of maps (atom or string keys), `%{columns, rows}` as a query result has them, and Ecto queries grouped in PostgreSQL, MySQL or SQLite; decimals, dates and times as they are; only the fields a chart draws are read |
| Looks | titles, subtitles, captions and sources; labels at line ends with leaders when they would touch; value labels; category names that wrap or stagger before they thin; a validated color order for every series, in light and dark |
| Notes | labels with a line to their point, bands of a range, lines with words: `Fude.annotate(chart, :label, x: ~D[2026-03-01], y: 76_000, text: "The new shop opens")` |
| Small multiples | `Fude.facet(chart, :region, columns: 3)`: a panel for each value, on shared scales, with one legend |
| Maps | the world's countries (Natural Earth, Equal Earth) and the US states (Albers USA), by ISO code, number code, postal code or name; loaded only by the pages that draw them |
| Live | charts follow their assigns and move; `Fude.Live.append/5` streams rows into a window of the last `max` |
| Specs | `Fude.Spec.dump/1` and `load/1`: JSON with a version, migrated forward; errors with their path and a suggestion (`did you mean "sum"?`) |
| Interaction | a crosshair and one tooltip for every series on lines and areas, a tooltip on each mark of the others, a legend that highlights and hides, keyboard navigation with announcements |
| Collaboration | `Fude.Issho`: a chart as an Issho room's shared state, changed by people with `Fude`'s functions and by agents with `read_chart` and `change_chart`, always valid; the others' pointers on its marks |
| Export | `<.chart export>` buttons and `Fude.Live.export/3`: SVG, and PNG at twice the size |
| Locales | numbers, currencies and dates in the chart's locale, with the browser's `Intl` |
| Theming | CSS custom properties, the Sumi theme, dark mode, reduced motion, forced colors |
| Accessibility | a description for screen readers, keyboard navigation through the data, live announcements, a table of the data, contrast-checked colors |
## Performance
Measured with `mix run bench/compile.exs`: the time the server takes to
make a chart of rows in memory, on 200,000 rows.
| | |
| --- | --- |
| Bars, summed by category and color | 0.16 s |
| A line by month | 0.18 s |
| A histogram of 40 bins | 0.23 s |
| A scatter of every row | 0.21 s |
* An Ecto query does its grouping in the database: the server reads the
groups, not the rows.
* The browser draws the marks on a canvas and its words as SVG; it draws
again only what moves, at 60 frames a second.
## Install
Fude needs Elixir 1.18 or later (it uses the built-in `JSON` module),
Phoenix 1.7.14 or 1.8, and Phoenix LiveView 1.1 or later. A chart of an
Ecto query needs Ecto 3.13 or later.
Add `fude` to the deps in `mix.exs`:
```elixir
def deps do
[
{:fude, "~> 0.1"}
]
end
```
Then run:
```sh
mix deps.get
mix fude.install
```
The installer adds the hook to `assets/js/app.js` and the style sheet to
`assets/css/app.css`, and prints what it changed. Give `--dry-run` to see
the changes first. It does not add what a file already has, so you can
run it again.
### The manual steps
To make the same changes by hand, import the hook in `assets/js/app.js`
and add it to the hooks of the `LiveSocket`:
```js
import { Fude } from "fude"
const liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks, Fude},
})
```
Import the style sheet in `assets/css/app.css` (and the Sumi theme, if you
want it: Sumi is the design system of Hattori AI, the makers of Fude,
Masu, Kotoba, Koyomi and Jumon). Put these lines below the other `@import`
lines of `app.css`:
```css
@import "../../deps/fude/priv/static/fude.css";
/* @import "../../deps/fude/priv/static/fude-sumi.css"; */
```
For map charts, register the maps the app draws (see the
[Maps](guides/maps.md) guide):
```js
import { registerMap } from "fude"
import world from "fude/maps/world.json"
registerMap("world", world)
```
### `NODE_PATH=deps`
`import { Fude } from "fude"` resolves through the `NODE_PATH` of your
esbuild profile, which must have `deps/`. The Phoenix generators already
set it. If your profile has no `NODE_PATH`, add it in `config/config.exs`:
```elixir
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
```
## Use
```heex
<.chart
id="revenue"
chart={
Fude.line(@sales, x: {:month, :placed_on}, y: {:sum, :total}, color: :channel)
|> Fude.title("Online keeps growing", subtitle: "Monthly revenue by channel")
}
/>
```
Import the components in the `html_helpers` of your web module:
```elixir
import Fude.Components
```
A chart of an Ecto query, grouped in the database:
```elixir
Order
|> where(shop_id: ^shop.id)
|> Fude.query(Repo)
|> Fude.bar(x: :status, y: {:count, :id})
```
A chart saved as data, and drawn again with its data:
```elixir
spec = Fude.Spec.dump(chart)
# later
<.chart id="revenue" spec={report.chart} data={@sales} />
```
When `@sales` changes, the chart draws again in place: its marks move
from where they were.
## Guides
* [Quickstart](guides/quickstart.md) - a first chart, step by step.
* [Charts](guides/charts.md) - the kinds, their channels and options, layers.
* [Data](guides/data.md) - lists, query results, Ecto queries in the database, Masu's grids.
* [Telling the story](guides/story.md) - words, notes on the data, small multiples.
* [Maps](guides/maps.md) - regions colored by a value, circles on a map.
* [Specs](guides/specs.md) - a chart as data: saving, loading, versions, errors.
* [Live](guides/live.md) - charts that follow their data, streamed points, export.
* [Collaboration](guides/collaboration.md) - shared charts with Issho, for people and agents.
* [Testing](guides/testing.md) - `Fude.Test` in `Phoenix.LiveViewTest`.
* [Theming](guides/theming.md) - the custom properties, dark mode, the Sumi theme.
* [Accessibility](guides/accessibility.md) - what a person who does not see the chart gets.
* [Known limits](guides/limits.md) - what Fude does not do yet.
## Development
```sh
mix deps.get
mix fude.build # the bundle and the style sheets, into priv/static
mix dev # the gallery at http://localhost:4099
```
### Tests
```sh
mix test # Elixir, with SQLite for the queries
FUDE_DB=postgres mix test # the queries on PostgreSQL
FUDE_DB=mysql mix test # and on MySQL
mix test --include build # the build and the package
mix test.js # the browser's rules, maps and merges
```
The rules that both the server and the browser apply (ticks, nice
domains, bins, units of time) are tested on both sides with one fixture,
`test/fixtures/rules.json`.
### Release
`mix release` builds the bundle, checks `priv/static` and the version of
`package.json`, publishes to Hex and tags the version.
## Contributing
See [CONTRIBUTING.md](https://github.com/hattoriai/fude/blob/main/CONTRIBUTING.md): when a change needs an issue
first, and what a pull request needs.
## License
Fude is released under the MIT License. See `LICENSE`. The maps are made
from Natural Earth and the U.S. Census Bureau (public domain), through
world-atlas and us-atlas (ISC) and i18n-iso-countries (MIT). See `NOTICE`.