Bin
The Bin family holds the ways to bin a key into cells. Bin.step and Bin.thresholds cut the line of one field into intervals: they are the argument of field(x).bin(...). Bin.hex and Bin.voronoi divide the plane of two fields into polygons: they are the argument of struct({ x, y }).bin(...), a key that reads two fields at once. partition over a binned key gives each group its cell, and region draws it.
import { Bin, Color, chart, field, partition, region, struct } from "gofish-graphics";
import data from "vega-datasets";
const contiguous = (airports: any[]) =>
airports.filter(
(d) => d.longitude > -130 && d.latitude > 24 && d.latitude < 50
);
(async () => {
const loaded = { airports: await data["airports.csv"]() };
const context = { loaded };
const container = document.getElementById("app");
chart(contiguous(context.loaded.airports), {
color: Color.gradient("blues"),
axes: true,
})
.flow(
partition({
by: struct({ x: "longitude", y: "latitude" }).bin(
Bin.hex({ radius: 1 })
),
})
)
.mark(region({ fill: field("longitude").count(), stroke: "white" }))
.render(container, { w: 600, h: 360 });
})();Signature
field(x).bin(b);
struct({ x, y }).bin(b);
Bin.step(size);
Bin.thresholds(countOrEdges);
Bin.hex({ radius });
Bin.voronoi({ seeds });A kind is valid only on its key: Bin.step and Bin.thresholds on field(x).bin, Bin.hex and Bin.voronoi on struct({ x, y }).bin. The other way round is a type error in TypeScript, and an error that names the right key when the chart renders. field(x).bin also takes a Calendar value, for a time column, or a list of Calendar values (a product, [Calendar.year, Calendar.month]). A second .bin(...) on a field splits the cells of the first (a chain, field(x).bin(Bin.step(10)).bin(Bin.step(2))).
struct({ x, y }) takes two field names: x is the column read on the x axis, and y the column read on the y axis. The name follows polars' pl.struct. .bin(b) chains on it, as .bin(p) does on field, and takes a call in the Bin family. A struct takes one bin. A struct is a key only once it is binned: partition({ by: struct({ x, y }) }) is a type error in TypeScript, and an error when the chart renders.
Each member of the family returns an object whose own keys are { kind, ...params }, of type Bin.Bin, for example { kind: "hex", radius: 0.5 } or { kind: "step", size: 0.5 }. Those keys are the bin's wire form. Bin.step and Bin.thresholds also have .format(fn), which labels their cells.
The family is also a module of its own:
import { Bin, struct } from "gofish-graphics";
import { hex } from "gofish-graphics/bin"; // the same function as Bin.hexMembers
| Member | Key | Its cells |
|---|---|---|
Bin.step(size) | field(x) | Intervals size wide, starting at multiples of size. |
Bin.thresholds(countOrEdges) | field(x) | About count intervals with round edges, or the given edges. |
Bin.hex({ radius }) | struct({ x, y }) | A grid of hexagons that covers the domain of the two fields. |
Bin.voronoi({ seeds }) | struct({ x, y }) | One cell per seed: the points nearer to it than to other seeds. |
The cells are those of the domain of the key's columns in the chart's data, so every group of a nested split sees the same cells, and an empty cell is kept. A row that is missing a field of the key is dropped.
Bin.step
Bin.step(size: number)| Param | Type | Description |
|---|---|---|
size | number | The width of each cell, in data units: finite and above 0. |
A value is in the cell that starts at the largest multiple of size at or below it, whatever the domain. A cell holds its start and not its end.
partition({ by: field("rating").bin(Bin.step(0.5)), dir: "x" });import { Bin, chart, field, partition, rect } from "gofish-graphics";
const movies = [
1.2, 1.6, 1.8, 2.6, 2.9, 3.1, 3.4, 3.6, 3.8, 4.0, 4.1, 4.3, 4.4, 4.6, 4.7,
4.8, 4.9, 5.0, 5.1, 5.2, 5.3, 5.3, 5.4, 5.5, 5.6, 5.6, 5.7, 5.8, 5.9, 5.9,
6.0, 6.0, 6.1, 6.2, 6.2, 6.3, 6.3, 6.4, 6.4, 6.5, 6.5, 6.5, 6.6, 6.6, 6.7,
6.7, 6.8, 6.8, 6.9, 6.9, 7.0, 7.0, 7.1, 7.2, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7,
7.8, 7.9, 8.0, 8.1, 8.2, 8.4, 8.5, 8.7, 8.9, 9.1, 9.3, 9.5,
].map((rating) => ({ rating }));
const container = document.getElementById("app");
chart(movies, { axes: true })
.flow(partition({ by: field("rating").bin(Bin.step(0.5)), dir: "x" }))
.mark(rect({ h: field("rating").count(), fill: "steelblue", inset: 0.5 }))
.render(container, { w: 560, h: 200 });Bin.thresholds
Bin.thresholds(countOrEdges: number | number[])| Param | Type | Description |
|---|---|---|
countOrEdges | number | number[] | A count, 1 or more: about that many cells, with the width a numeric axis with that many ticks uses. Or a list of edges to cut the domain at. |
The cells are fitted to the domain of the field in the chart's data, and cover it with both ends included: the largest value falls in the last cell, as in numpy's histogram and d3's bin. With a list of edges, the domain's ends are the outer edges, and an edge outside the domain cuts nothing. field(x).bin() with no argument is field(x).bin(Bin.thresholds(10)).
spread({ by: field("age").bin(Bin.thresholds(10)), dir: "x" });
spread({ by: field("age").bin(Bin.thresholds([18, 30, 50])), dir: "x" });Cell labels
Bin.step(...) and Bin.thresholds(...) take .format(fn), as a Calendar value does. fn is a function of the cell, and returns its label. The default label is the cell's two edges, "0.5–1".
field("age").bin(Bin.step(10).format((c) => `${c.start}s`));A function cannot be serialized, so a bin with a format is JS-only: writing it to the IR or sending it to Python is an error.
Bin.hex
Bin.hex({ radius: number | { x: number, y: number } });| Param | Type | Description |
|---|---|---|
radius | number | { x: number, y: number } | The distance from a hexagon's center to its corners, in data units, above 0. One number when both fields share a unit, or one per field when they do not. |
The hexagons have a corner at the top (pointy-top), as in d3-hexbin, ggplot2's geom_hex and Observable Plot. One hexagon is centered on the origin (0, 0), so the grid does not move when the data does. A radius per axis, as in ggplot2's binwidth = c(x, y), scales each hexagon by the x radius on x and the y radius on y:
struct({ x: "Miles_per_Gallon", y: "Weight_in_lbs" }).bin(
Bin.hex({ radius: { x: 1.5, y: 250 } })
);import { Bin, Color, chart, field, partition, region, struct } from "gofish-graphics";
import data from "vega-datasets";
(async () => {
const loaded = { cars: await data["cars.json"]() };
const context = { loaded };
const container = document.getElementById("app");
const cars = (context.loaded.cars as any[]).filter(
(d) => d.Miles_per_Gallon != null && d.Weight_in_lbs != null
);
chart(cars, { color: Color.gradient("viridis"), axes: true })
.flow(
partition({
by: struct({ x: "Miles_per_Gallon", y: "Weight_in_lbs" }).bin(
Bin.hex({ radius: { x: 1.5, y: 250 } })
),
})
)
.mark(region({ fill: field("Miles_per_Gallon").count() }))
.render(container, { w: 480, h: 360 });
})();The cells are every hexagon whose inside meets the box of the domain, plus the hexagon of each row. A point on an edge between two hexagons goes to one of them, the same one every time. The hexagons are regular in data, so they look regular on screen only when the two scales match.
Bin.voronoi
Bin.voronoi({ seeds: Record<string, unknown>[] })| Param | Type | Description |
|---|---|---|
seeds | Record<string, unknown>[] | The seed rows, with the same two fields as the key. Pass the chart's own data to give each row its own cell. |
Each row goes to its nearest seed. The cells are clipped to the box that holds the data and the seeds, so every seed has a cell, and seeds at the same point share one cell. Distances are measured in data units, so the cells are meaningful when the two fields share a unit, such as longitude and latitude.
partition({
by: struct({ x: "longitude", y: "latitude" }).bin(
Bin.voronoi({ seeds: hubs })
),
});import { Bin, Color, chart, circle, field, layer, partition, region, scatter, struct } from "gofish-graphics";
import data from "vega-datasets";
const contiguous = (airports: any[]) =>
airports.filter(
(d) => d.longitude > -130 && d.latitude > 24 && d.latitude < 50
);
const HUBS = [
"ATL", "BOS", "CLT", "DEN", "DFW", "DTW", "IAH", "JFK", "LAX", "MCI",
"MIA", "MSP", "MSY", "ORD", "PDX", "PHX", "SEA", "SFO", "SLC", "STL",
];
(async () => {
const loaded = { airports: await data["airports.csv"]() };
const context = { loaded };
const container = document.getElementById("app");
const airports = contiguous(context.loaded.airports);
const hubs = airports.filter((d) => HUBS.includes(d.iata));
layer([
chart(airports, { color: Color.gradient("reds") })
.flow(
partition({
by: struct({ x: "longitude", y: "latitude" }).bin(
Bin.voronoi({ seeds: hubs })
),
})
)
.mark(region({ fill: field("iata").count(), stroke: "white" })),
chart(hubs)
.flow(scatter({ x: "longitude", y: "latitude" }))
.mark(circle({ r: 3, fill: "black" })),
]).render(container, { w: 600, h: 360, axes: true });
})();With seeds the chart's own data, each row has its own cell: the region nearer to it than to any other row.
import { Bin, chart, circle, layer, partition, region, scatter, struct } from "gofish-graphics";
import { penguins } from "./dataset";
const beaked = penguins.filter(
(d) => d["Beak Length (mm)"] !== null && d["Beak Depth (mm)"] !== null
);
const container = document.getElementById("app");
const key = struct({ x: "Beak Length (mm)", y: "Beak Depth (mm)" });
layer([
chart(beaked)
.flow(partition({ by: key.bin(Bin.voronoi({ seeds: beaked })) }))
.mark(region({ fill: "#f4f1ea", stroke: "#b9b2a3", strokeWidth: 0.5 })),
chart(beaked)
.flow(scatter({ x: "Beak Length (mm)", y: "Beak Depth (mm)" }))
.mark(circle({ r: 2.5, fill: "Species" })),
]).render(container, { w: 480, h: 360, axes: true });Dataset
export type Penguin = {
Species: string;
Island: string;
"Beak Length (mm)": number | null;
"Beak Depth (mm)": number | null;
"Flipper Length (mm)": number | null;
"Body Mass (g)": number | null;
Sex: string | null;
};
export const penguins = [
{
Species: "Adelie",
Island: "Torgersen",
"Beak Length (mm)": 39.1,
"Beak Depth (mm)": 18.7,
"Flipper Length (mm)": 181,
"Body Mass (g)": 3750,
Sex: "MALE",
},
{
Species: "Adelie",
Island: "Torgersen",
"Beak Length (mm)": 39.5,
"Beak Depth (mm)": 17.4,
"Flipper Length (mm)": 186,
"Body Mass (g)": 3800,
Sex: "FEMALE",
},
{
Species: "Adelie",
Island: "Torgersen",
"Beak Length (mm)": 40.3,
"Beak Depth (mm)": 18,
"Flipper Length (mm)": 195,
"Body Mass (g)": 3250,
Sex: "FEMALE",
},
{
Species: "Adelie",
Island: "Torgersen",
"Beak Length (mm)": null,
"Beak Depth (mm)": null,
"Flipper Length (mm)": null,
"Body Mass (g)": null,
Sex: null,
},
{
Species: "Adelie",
Island: "Torgersen",
"Beak Length (mm)": 36.7,
"Beak Depth (mm)": 19.3,
"Flipper Length (mm)": 193,
"Body Mass (g)": 3450,
Sex: "FEMALE",
},
// … 339 more rows
];Uses d3-delaunay, which builds on Delaunator. See Underlying Space for how the cells are made and handed to each group.
