Skip to content

partition ​

Divides the space into the cells of a binned key, one group per cell, and gives each group its cell. Each cell sits at its true place on one continuous scale, so a cell's width follows its width in data: a 29-day February is narrower than a 31-day March. An empty cell keeps its place. With one key per axis, it divides both axes into rectangles. With two fields binned together, it divides the plane into hexagons or Voronoi cells.

ts
import { Calendar, Schema, chart, field, partition, rect } from "gofish-graphics";

const DAY = 864e5;

const allDaily = (() => {
  const out: { date: string; region: string; value: number }[] = [];
  const start = Date.UTC(2024, 0, 1);
  const end = Date.UTC(2024, 7, 31);
  const regions = ["North", "South", "West"];
  for (let t = start, i = 0; t <= end; t += DAY, i++) {
    const date = new Date(t).toISOString().slice(0, 10);
    regions.forEach((region, r) => {
      const value =
        10 + 4 * Math.sin(i / 20 + r) + 2 * Math.sin(i / 3.1 + 2 * r) + 3 * r;
      out.push({ date, region, value: Math.round(value * 10) / 10 });
    });
  }
  return out;
})();

const container = document.getElementById("app");
chart(allDaily, { schema: { date: Schema.time() }, axes: true })
  .flow(partition({ by: field("date").bin([Calendar.year, Calendar.month]), dir: "x" }))
  .mark(rect({ h: field("value").sum(), fill: "steelblue", inset: 1 }))
  .render(container, { w: 560, h: 200 });

Signature ​

ts
partition({ by, dir, alignment?, axes? })
partition({ by: { x, y }, axes? })
partition({ by: struct({ x, y }).bin(b), axes? })

partition has only the operator form, used inside .flow(...). Its children are the groups of its key, so there is no list of children to pass it.

Parameters ​

Divide the space into the cells of a binned key, and give each group its cell. Each cell sits at its true place on one continuous scale, so a cell's width follows its width in data, and an empty cell keeps its place. A mark with no size of its own fills its cell, and a mark with a size of its own is centered in it.

OptionTypeDefaultDescription
byFieldAccessor | StructAccessorRequired. A key that has a region: a binned field whose cells lie on the line, whose cells divide the axis dir: field(x).bin(p) with p Bin.step(size), Bin.thresholds(countOrEdges), or a Calendar level that is a part of nothing (Calendar.year, Calendar.week); a product that starts with one, field(x).bin([Calendar.year, Calendar.month]); or a chain that starts with one, field(x).bin(Calendar.year).bin(Calendar.month). A part of a cycle (field(x).bin(Calendar.month), the month of the year) has no one region on the time line and is an error. Or one binned field per axis, { x: field(a).bin(p), y: field(b).bin(q) }, whose cells divide both axes into rectangles; this is the partition on x, then the partition on y, and it is written as those two partitions. Or two fields binned together, struct({ x: a, y: b }).bin(Bin.hex({ radius })) or .bin(Bin.voronoi({ seeds })), whose cells are polygons over both axes. A plain field, or a struct with no bin, has no region and is an error.
dirstringAxis to divide: x, y, or an axis name the enclosing coordinate space declares (polar theta/r). Required with a single key, and not allowed with a key per axis.
alignmentstring"baseline"Alignment of the children on the other axis ("start" | "middle" | "end" | "baseline"). Applies only where nothing gives the children a cell on that axis: inside a cell of another partition, each child is placed in that cell. Not allowed with a key per axis.
axesAxesOptions

The key must have a region ​

by must be a key whose values have a region. That is a binned field, field(x).bin(p) whose values are cells of the line, each an interval [start, end), or a binned struct, struct({ x, y }).bin(b), whose values are cells of the plane (see Cells of the plane). See the field-expression pipeline for the partitions .bin takes.

ts
partition({ by: field("date").bin([Calendar.year, Calendar.month]), dir: "x" }); // true month widths
partition({
  by: field("date").bin(Calendar.year).bin(Calendar.month),
  dir: "x",
}); // the same cells
partition({ by: field("rating").bin(Bin.step(0.5)), dir: "x" }); // a histogram

A level binned alone that is a part of a larger level is a part of a cycle, with no one region on the time line: field("date").bin(Calendar.month) is the month of the year, every June in one cell. So partition({ by: field("date").bin(Calendar.month), dir: "x" }) is a type error, and an error naming the product and the chain when the chart renders.

A plain field has no region, so partition({ by: "date", dir: "x" }) and partition({ by: field("date"), dir: "x" }) are type errors in TypeScript, and errors when the chart renders. So is a struct with no bin, partition({ by: struct({ x: "lon", y: "lat" }) }). To give each value an equal slot instead, use spread with the same key.

Filling the cell ​

A mark with no size along dir fills its cell. rect is the mark for this. Its inset option draws it in from the edges of the cell by a number of pixels, so neighboring bars are drawn apart. A mark with a size of its own along dir, such as a circle or a text, keeps its size and is centered in the cell.

An operator after partition divides the cell it is given. A stack fills the cell's width, and a spread fits its groups inside the cell, so February's group is narrower than March's.

ts
import { Calendar, Schema, chart, field, partition, rect, spread } from "gofish-graphics";

const DAY = 864e5;

const allDaily = (() => {
  const out: { date: string; region: string; value: number }[] = [];
  const start = Date.UTC(2024, 0, 1);
  const end = Date.UTC(2024, 7, 31);
  const regions = ["North", "South", "West"];
  for (let t = start, i = 0; t <= end; t += DAY, i++) {
    const date = new Date(t).toISOString().slice(0, 10);
    regions.forEach((region, r) => {
      const value =
        10 + 4 * Math.sin(i / 20 + r) + 2 * Math.sin(i / 3.1 + 2 * r) + 3 * r;
      out.push({ date, region, value: Math.round(value * 10) / 10 });
    });
  }
  return out;
})();

const container = document.getElementById("app");
chart(allDaily, { schema: { date: Schema.time() }, axes: true })
  .flow(
    partition({ by: field("date").bin([Calendar.year, Calendar.month]), dir: "x" }),
    spread({ by: "region", dir: "x" })
  )
  .mark(rect({ h: field("value").sum(), fill: "region" }))
  .render(container, { w: 560, h: 200 });

Both axes ​

With by keyed by axis, partition divides both axes. Each key must have a region, and dir and alignment are not allowed.

ts
partition({
  by: { x: field("a").bin(Bin.step(1)), y: field("b").bin(Bin.step(0.5)) },
});

It is the same as the 1D partition on x, then the 1D partition on y:

ts
(partition({ by: field("a").bin(Bin.step(1)), dir: "x" }),
  partition({ by: field("b").bin(Bin.step(0.5)), dir: "y" }));

A partition inside a cell of another partition gives each of its groups that cell on the other axis, so nesting the two in either order places the groups the same way. There alignment has nothing to align.

Each group gets a rectangle: its x cell by its y cell. A region fills it, so a 2D histogram is a region whose fill is a count.

ts
import { Bin, Color, chart, field, partition, region } from "gofish-graphics";
import data from "vega-datasets";

(async () => {
  const loaded = { movies: await data["movies.json"]() };
  const context = { loaded };
  const container = document.getElementById("app");
  const movies = (context.loaded.movies as any[]).filter(
    (d) => d["IMDB Rating"] != null && d["Rotten Tomatoes Rating"] != null
  );
  chart(movies, { color: Color.gradient("blues"), axes: true })
    .flow(
      partition({
        by: {
          x: field("IMDB Rating").bin(Bin.step(0.5)),
          y: field("Rotten Tomatoes Rating").bin(Bin.step(5)),
        },
      })
    )
    .mark(region({ fill: field("IMDB Rating").count() }))
    .render(container, { w: 480, h: 320 });
})();

A mark with a size of its own is centered in its rectangle.

ts
import { Bin, chart, field, partition, text } from "gofish-graphics";
import { penguins } from "./dataset";

const measured = penguins.filter(
  (d) => d["Flipper Length (mm)"] !== null && d["Body Mass (g)"] !== null
);

const container = document.getElementById("app");
chart(measured, { axes: true })
  .flow(
    partition({
      by: {
        x: field("Flipper Length (mm)").bin(Bin.step(10)),
        y: field("Body Mass (g)").bin(Bin.step(500)),
      },
    })
  )
  .mark(text({ text: field("Body Mass (g)").count() }))
  .render(container, { w: 420, h: 320 });
Dataset
ts
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
];

An empty cell is a group with no rows, so a count over it is 0, and the mark draws that 0.

Cells of the plane ​

With by a binned struct, partition divides both axes at once into the cells of a Bin call: Bin.hex({ radius }) for a grid of hexagons, or Bin.voronoi({ seeds }) for one cell per seed. dir and alignment are not allowed.

ts
partition({ by: struct({ x: "lon", y: "lat" }).bin(Bin.hex({ radius: 0.5 })) });

Each group gets its cell's outline. A region draws it, so a hexbin is a region whose fill is a count. As in 1D, the cells are those of the two columns' domain in the chart's data, and an empty hexagon is a group with no rows, so it is drawn with a count of 0.

ts
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 });
})();

A mark with a size of its own is centered in its cell's box, which for a hexagon is its center.

ts
import { Bin, Color, chart, field, layer, partition, region, struct, text } 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");
  const airports = contiguous(context.loaded.airports);
  chart(airports, { color: Color.gradient("blues") })
    .flow(
      partition({
        by: struct({ x: "longitude", y: "latitude" }).bin(
          Bin.hex({ radius: 3 })
        ),
      })
    )
    .mark(
      layer([
        region({ fill: field("iata").count(), stroke: "white" }),
        text({ text: field("iata").count(), fontSize: 10 }),
      ])
    )
    .render(container, { w: 600, h: 360, axes: true });
})();

With Bin.voronoi, each row goes to its nearest seed.

ts
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 });
})();

Axis ​

The axis along dir is continuous, and with a key per axis or a binned struct both axes are. A binned time column gives a time axis whose inner row is the cells' own partition, with each label centered under its cell, and an outer row of the parent level (years under months). A binned number column gives a numeric axis. See Underlying Space for how the layout and the axis read the cells.