Skip to content

Added density reducer to bin/group/hexbin #2047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/transforms/bin.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ Plot.plot({
```
:::

The bin transform works with Plot’s [faceting system](../features/facets.md), partitioning bins by facet. Below, we compare the weight distributions of athletes within each sport using the *proportion-facet* reducer. Sports are sorted by median weight: gymnasts tend to be the lightest, and basketball players the heaviest.
The bin transform works with Plot’s [faceting system](../features/facets.md), partitioning bins by facet. Below, we compare the weight distributions of athletes within each sport using the *density* reducer. Sports are sorted by median weight: gymnasts tend to be the lightest, and basketball players the heaviest.

:::plot defer
```js-vue
Expand All @@ -202,7 +202,7 @@ Plot.plot({
x: {grid: true},
fy: {domain: d3.groupSort(olympians.filter((d) => d.weight), (g) => d3.median(g, (d) => d.weight), (d) => d.sport)},
color: {scheme: "{{$dark ? "turbo" : "YlGnBu"}}"},
marks: [Plot.rect(olympians, Plot.binX({fill: "proportion-facet"}, {x: "weight", fy: "sport", inset: 0.5}))]
marks: [Plot.rect(olympians, Plot.binX({fill: "density"}, {x: "weight", fy: "sport", inset: 0.5}))]
})
```
:::
Expand Down Expand Up @@ -253,6 +253,7 @@ The following named reducers are supported:
* *first* - the first value, in input order
* *last* - the last value, in input order
* *count* - the number of elements (frequency)
* *density* – the count or sum normalized by series (*z*)
* *distinct* - the number of distinct values
* *sum* - the sum of values
* *proportion* - the sum proportional to the overall total (weighted frequency)
Expand Down
1 change: 1 addition & 0 deletions docs/transforms/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ The following named reducers are supported:
* *first* - the first value, in input order
* *last* - the last value, in input order
* *count* - the number of elements (frequency)
the count or sum normalized by series (*z*)
* *sum* - the sum of values
* *proportion* - the sum proportional to the overall total (weighted frequency)
* *proportion-facet* - the sum proportional to the facet total
Expand Down
1 change: 1 addition & 0 deletions docs/transforms/hexbin.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ The following named reducers are supported:
* *first* - the first value, in input order
* *last* - the last value, in input order
* *count* - the number of elements (frequency)
the count or sum normalized by series (*z*)
* *distinct* - the number of distinct values
* *sum* - the sum of values
* *proportion* - the sum proportional to the overall total (weighted frequency)
Expand Down
2 changes: 2 additions & 0 deletions src/reducer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type ReducerPercentile =
* - *first* - the first value, in input order
* - *last* - the last value, in input order
* - *count* - the number of elements (frequency)
* - *density* – the count or sum normalized by series (*z*)
* - *distinct* - the number of distinct values
* - *sum* - the sum of values
* - *proportion* - the sum proportional to the overall total (weighted frequency)
Expand All @@ -36,6 +37,7 @@ export type ReducerName =
| "last"
| "identity"
| "count"
| "density"
| "distinct"
| "sum"
| "proportion"
Expand Down
1 change: 1 addition & 0 deletions src/transforms/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ function binn(
if (sort) sort.scope("facet", facet);
if (filter) filter.scope("facet", facet);
for (const [f, I] of maybeGroup(facet, G)) {
for (const o of outputs) o.scope("group", I);
for (const [k, g] of maybeGroup(I, K)) {
for (const [b, extent] of bin(g)) {
if (G) extent.z = f;
Expand Down
15 changes: 15 additions & 0 deletions src/transforms/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ function groupn(
if (sort) sort.scope("facet", facet);
if (filter) filter.scope("facet", facet);
for (const [f, I] of maybeGroup(facet, G)) {
for (const o of outputs) o.scope("group", I);
for (const [y, gg] of maybeGroup(I, Y)) {
for (const [x, g] of maybeGroup(gg, X)) {
const extent = {data};
Expand Down Expand Up @@ -248,6 +249,8 @@ export function maybeReduce(reduce, value, fallback = invalidReduce) {
return reduceIdentity;
case "count":
return reduceCount;
case "density":
return reduceDensity;
case "distinct":
return reduceDistinct;
case "sum":
Expand Down Expand Up @@ -405,6 +408,18 @@ export const reduceCount = {
}
};

export const reduceDensity = {
label: "Density",
scope: "group",
reduceIndex(I, V, context, extent) {
if (context === undefined) return I.length;
let proportion = I.length / context;
if ("y2" in extent && !("x2" in extent)) proportion /= extent.y2 - extent.y1;
else if ("x2" in extent && !("y2" in extent)) proportion /= extent.x2 - extent.x1;
return proportion;
}
};

const reduceDistinct = {
label: "Distinct",
reduceIndex(I, X) {
Expand Down
1 change: 1 addition & 0 deletions src/transforms/hexbin.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function hexbin(outputs = {fill: "count"}, {binWidth, ...options} = {}) {
const binFacet = [];
for (const o of outputs) o.scope("facet", facet);
for (const [f, I] of maybeGroup(facet, G)) {
for (const o of outputs) o.scope("group", I);
for (const {index: b, extent} of hbin(data, I, X, Y, binWidth)) {
binFacet.push(++i);
BX.push(extent.x);
Expand Down
178 changes: 178 additions & 0 deletions test/output/densityReducer.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/plots/athletes-sport-weight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function athletesSportWeight() {
grid: true,
color: {scheme: "YlGnBu", zero: true},
marks: [
Plot.barX(athletes, Plot.binX({fill: "proportion-facet"}, {x: "weight", fy: "sport", thresholds: 60})),
Plot.barX(athletes, Plot.binX({fill: "density"}, {x: "weight", fy: "sport", thresholds: 60})),
Plot.frame({anchor: "bottom", facetAnchor: "bottom"})
]
});
Expand Down
46 changes: 46 additions & 0 deletions test/plots/density-reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";

const pdf_normal = (x, mu = 0, sigma = 1) =>
Math.exp(-0.5 * Math.pow((x - mu) / sigma, 2)) / (sigma * Math.sqrt(2 * Math.PI));

const densities = d3
.range(-6, 10, 0.1)
.map((x) => [0, 3].map((mu) => [1, 2].map((sigma) => ({x, mu, sigma, rho: pdf_normal(x, mu, sigma)}))))
.flat(3);

const n_pts = 100000;

const mus = Array.from({length: n_pts}, d3.randomBernoulli.source(d3.randomLcg(42))(0.2)).map((x) => 3 * x);
const sigmas = Array.from({length: n_pts}, d3.randomBernoulli.source(d3.randomLcg(43))(0.3)).map((x) => 1 + x);
const standardNormals = Array.from({length: n_pts}, d3.randomNormal.source(d3.randomLcg(44))(0, 1)).map(
(x, i) => x * sigmas[i] + mus[i]
);

const pts = standardNormals.map((value, i) => ({mu: mus[i], sigma: sigmas[i], value}));

export async function densityReducer() {
return Plot.plot({
marks: [
Plot.areaY(
pts,
Plot.binX(
{y2: "density"},
{
x: "value",
fill: (x) => `μ = ${x.mu}`,
opacity: 0.5,
fy: (x) => `σ = ${x.sigma}`,
interval: 0.2,
curve: "step"
}
)
),
Plot.line(densities, {x: "x", y: "rho", stroke: (x) => `μ = ${x.mu}`, fy: (x) => `σ = ${x.sigma}`}),
Plot.ruleY([0])
],
fy: {label: null},
color: {legend: true, type: "categorical"},
grid: true
});
}
2 changes: 1 addition & 1 deletion test/plots/hexbin-r.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function hexbinR() {
marks: [
Plot.frame(),
Plot.hexgrid(),
Plot.dot(penguins, Plot.hexbin({title: "count", r: "count", fill: "proportion-facet"}, xy))
Plot.dot(penguins, Plot.hexbin({title: "count", r: "count", fill: "density"}, xy))
]
});
}
1 change: 1 addition & 0 deletions test/plots/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export * from "./d3-survey-2015-comfort.js";
export * from "./d3-survey-2015-why.js";
export * from "./darker-dodge.js";
export * from "./decathlon.js";
export * from "./density-reducer.js";
export * from "./diamonds-boxplot.js";
export * from "./diamonds-carat-price-dots.js";
export * from "./diamonds-carat-price.js";
Expand Down