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/5without 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/1gives a map with a version to save in a column or a file, andFude.Spec.load/1takes 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
(
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 inPhoenix.LiveViewTestwith 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:
def deps do
[
{:fude, "~> 0.1"}
]
end
Then run:
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:
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:
@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 guide):
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:
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
Use
<.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:
import Fude.Components
A chart of an Ecto query, grouped in the database:
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:
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 - a first chart, step by step.
- Charts - the kinds, their channels and options, layers.
- Data - lists, query results, Ecto queries in the database, Masu's grids.
- Telling the story - words, notes on the data, small multiples.
- Maps - regions colored by a value, circles on a map.
- Specs - a chart as data: saving, loading, versions, errors.
- Live - charts that follow their data, streamed points, export.
- Collaboration - shared charts with Issho, for people and agents.
- Testing -
Fude.TestinPhoenix.LiveViewTest. - Theming - the custom properties, dark mode, the Sumi theme.
- Accessibility - what a person who does not see the chart gets.
- Known limits - what Fude does not do yet.
Development
mix deps.get
mix fude.build # the bundle and the style sheets, into priv/static
mix dev # the gallery at http://localhost:4099
Tests
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: 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.