Skip to content

Commit a3c84f0

Browse files
committed
fix #241; weighted group and bin
1 parent e7001f9 commit a3c84f0

File tree

7 files changed

+122
-29
lines changed

7 files changed

+122
-29
lines changed

src/mark.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,18 @@ export function lazyChannel(source) {
187187
return [
188188
{
189189
transform: () => value,
190-
label: typeof source === "string" ? source
191-
: source ? source.label
192-
: undefined
190+
label: labelof(source)
193191
},
194192
v => value = v
195193
];
196194
}
197195

196+
export function labelof(value, defaultValue) {
197+
return typeof value === "string" ? value
198+
: value && value.label !== undefined ? value.label
199+
: defaultValue;
200+
}
201+
198202
// Like lazyChannel, but allows the source to be null.
199203
export function maybeLazyChannel(source) {
200204
return source == null ? [source] : lazyChannel(source);

src/transforms/bin.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {bin as binner, cross, group} from "d3";
1+
import {bin as binner, cross, group, sum} from "d3";
22
import {firstof} from "../defined.js";
3-
import {valueof, first, second, range, identity, lazyChannel, maybeLazyChannel, maybeTransform, maybeColor, maybeValue, mid, take} from "../mark.js";
3+
import {valueof, first, second, range, identity, lazyChannel, maybeLazyChannel, maybeTransform, maybeColor, maybeValue, mid, take, labelof} from "../mark.js";
44
import {offset} from "../style.js";
55

66
// Group on y, z, fill, or stroke, if any, then bin on x.
@@ -43,12 +43,12 @@ export function bin({x, y, out = "fill", inset, insetTop, insetRight, insetBotto
4343
return {x1, x2, y1, y2, ...transform, inset, insetTop, insetRight, insetBottom, insetLeft, [out]: l};
4444
}
4545

46-
function bin1(x, key, {[key]: k, z, fill, stroke, domain, thresholds, normalize, cumulative, ...options} = {}) {
46+
function bin1(x, key, {[key]: k, z, fill, stroke, weight, domain, thresholds, normalize, cumulative, ...options} = {}) {
4747
const m = normalize === true || normalize === "z" ? 100 : +normalize;
4848
const bin = binof(identity, {value: x, domain, thresholds});
4949
const [X1, setX1] = lazyChannel(x);
5050
const [X2, setX2] = lazyChannel(x);
51-
const [L, setL] = lazyChannel(`Frequency${m === 100 ? " (%)" : ""}`);
51+
const [L, setL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`);
5252
const [vfill] = maybeColor(fill);
5353
const [vstroke] = maybeColor(stroke);
5454
const [BK, setBK] = maybeLazyChannel(k);
@@ -68,6 +68,7 @@ function bin1(x, key, {[key]: k, z, fill, stroke, domain, thresholds, normalize,
6868
const Z = valueof(data, z);
6969
const F = valueof(data, vfill);
7070
const S = valueof(data, vstroke);
71+
const W = valueof(data, weight);
7172
const binFacets = [];
7273
const binData = [];
7374
const X1 = setX1([]);
@@ -78,19 +79,19 @@ function bin1(x, key, {[key]: k, z, fill, stroke, domain, thresholds, normalize,
7879
const BZ = Z && setBZ([]);
7980
const BF = F && setBF([]);
8081
const BS = S && setBS([]);
81-
let n = data.length;
82+
let n = W ? sum(W) : data.length;
8283
let i = 0;
8384
if (cumulative < 0) B.reverse();
8485
for (const facet of facets) {
8586
const binFacet = [];
8687
for (const I of G ? group(facet, i => G[i]).values() : [facet]) {
87-
if (normalize === "z") n = I.length;
88+
if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length;
8889
const set = new Set(I);
8990
let f;
9091
for (const b of B) {
9192
const s = b.filter(i => set.has(i));
9293
f = cumulative && f !== undefined ? f.concat(s) : s;
93-
const l = f.length;
94+
const l = W ? sum(f, i => W[i]) : f.length;
9495
if (l > 0) {
9596
binFacet.push(i++);
9697
binData.push(take(data, f));
@@ -119,7 +120,7 @@ function bin1(x, key, {[key]: k, z, fill, stroke, domain, thresholds, normalize,
119120
// representing a field name, a function, an array), or the value and some
120121
// additional per-dimension binning options as an objects of the form {value,
121122
// domain?, thresholds?}.
122-
function bin2(x, y, {domain, thresholds, normalize, z, fill, stroke, ...options} = {}) {
123+
function bin2(x, y, {weight, domain, thresholds, normalize, z, fill, stroke, ...options} = {}) {
123124
const m = normalize === true || normalize === "z" ? 100 : +normalize;
124125
const binX = binof(first, {domain, thresholds, ...maybeValue(x)});
125126
const binY = binof(second, {domain, thresholds, ...maybeValue(y)});
@@ -128,7 +129,7 @@ function bin2(x, y, {domain, thresholds, normalize, z, fill, stroke, ...options}
128129
const [X2, setX2] = lazyChannel(x);
129130
const [Y1, setY1] = lazyChannel(y);
130131
const [Y2, setY2] = lazyChannel(y);
131-
const [L, setL] = lazyChannel(`Frequency${m === 100 ? " (%)" : ""}`);
132+
const [L, setL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`);
132133
const [vfill] = maybeColor(fill);
133134
const [vstroke] = maybeColor(stroke);
134135
const [BZ, setBZ] = maybeLazyChannel(z);
@@ -145,6 +146,7 @@ function bin2(x, y, {domain, thresholds, normalize, z, fill, stroke, ...options}
145146
const Z = valueof(data, z);
146147
const F = valueof(data, vfill);
147148
const S = valueof(data, vstroke);
149+
const W = valueof(data, weight);
148150
const binFacets = [];
149151
const binData = [];
150152
const X1 = setX1([]);
@@ -156,16 +158,16 @@ function bin2(x, y, {domain, thresholds, normalize, z, fill, stroke, ...options}
156158
const BZ = Z && setBZ([]);
157159
const BF = F && setBF([]);
158160
const BS = S && setBS([]);
159-
let n = data.length;
161+
let n = W ? sum(W) : data.length;
160162
let i = 0;
161163
for (const facet of facets) {
162164
const binFacet = [];
163165
for (const I of G ? group(facet, i => G[i]).values() : [facet]) {
164-
if (normalize === "z") n = I.length;
166+
if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length;
165167
const set = new Set(I);
166168
for (const b of B) {
167169
const f = b.filter(i => set.has(i));
168-
const l = f.length;
170+
const l = W ? sum(f, i => W[i]) : f.length;
169171
if (l > 0) {
170172
binFacet.push(i++);
171173
binData.push(take(data, f));

src/transforms/group.js

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {group as grouper, sort, InternSet} from "d3";
1+
import {group as grouper, sort, sum, InternSet} from "d3";
22
import {defined, firstof} from "../defined.js";
3-
import {valueof, maybeColor, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple} from "../mark.js";
3+
import {valueof, maybeColor, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof} from "../mark.js";
44

55
// Group on y, z, fill, or stroke, if any, then group on x.
66
export function groupX({x, y, out = y == null ? "y" : "fill", ...options} = {}) {
@@ -24,10 +24,10 @@ export function group({x, y, out = "fill", ...options} = {}) {
2424
return {x: X, y: Y, ...transform, [out]: L};
2525
}
2626

27-
function group1(x = identity, key, {[key]: k, domain, normalize, z, fill, stroke, ...options} = {}) {
27+
function group1(x = identity, key, {[key]: k, weight, domain, normalize, z, fill, stroke, ...options} = {}) {
2828
const m = normalize === true || normalize === "z" ? 100 : +normalize;
2929
const [X, setX] = lazyChannel(x);
30-
const [L, setL] = lazyChannel(`Frequency${m === 100 ? " (%)" : ""}`);
30+
const [L, setL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`);
3131
const [vfill] = maybeColor(fill);
3232
const [vstroke] = maybeColor(stroke);
3333
const [BK, setBK] = maybeLazyChannel(k);
@@ -48,6 +48,7 @@ function group1(x = identity, key, {[key]: k, domain, normalize, z, fill, stroke
4848
const Z = valueof(data, z);
4949
const F = valueof(data, vfill);
5050
const S = valueof(data, vstroke);
51+
const W = valueof(data, weight);
5152
const groupFacets = [];
5253
const groupData = [];
5354
const BX = setX([]);
@@ -57,15 +58,15 @@ function group1(x = identity, key, {[key]: k, domain, normalize, z, fill, stroke
5758
const BZ = Z && setBZ([]);
5859
const BF = F && setBF([]);
5960
const BS = S && setBS([]);
60-
let n = data.length;
61+
let n = W ? sum(W) : data.length;
6162
let i = 0;
6263
for (const facet of facets) {
6364
const groupFacet = [];
6465
for (const I of G ? grouper(facet, i => G[i]).values() : [facet]) {
65-
if (normalize === "z") n = I.length;
66+
if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length;
6667
for (const [x, f] of sort(grouper(I, i => X[i]), first)) {
6768
if (!defined(x)) continue;
68-
const l = f.length;
69+
const l = W ? sum(f, i => W[i]) : f.length;
6970
groupFacet.push(i++);
7071
groupData.push(take(data, f));
7172
BX.push(x);
@@ -86,14 +87,14 @@ function group1(x = identity, key, {[key]: k, domain, normalize, z, fill, stroke
8687
];
8788
}
8889

89-
function group2(xv, yv, {z, fill, stroke, domain, normalize, ...options} = {}) {
90+
function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} = {}) {
9091
let {value: x, domain: xdomain} = {domain, ...maybeValue(xv)};
9192
let {value: y, domain: ydomain} = {domain, ...maybeValue(yv)};
9293
([x, y] = maybeTuple(x, y));
93-
const k = normalize === true || normalize === "z" ? 100 : +normalize;
94+
const m = normalize === true || normalize === "z" ? 100 : +normalize;
9495
const [X, setX] = lazyChannel(x);
9596
const [Y, setY] = lazyChannel(y);
96-
const [L, setL] = lazyChannel(`Frequency${k === 100 ? " (%)" : ""}`);
97+
const [L, setL] = lazyChannel(`Frequency${m === 100 ? " (%)" : ""}`);
9798
const [Z, setZ] = maybeLazyChannel(z);
9899
const [vfill] = maybeColor(fill);
99100
const [vstroke] = maybeColor(stroke);
@@ -113,6 +114,7 @@ function group2(xv, yv, {z, fill, stroke, domain, normalize, ...options} = {}) {
113114
const Z = valueof(data, z);
114115
const F = valueof(data, vfill);
115116
const S = valueof(data, vstroke);
117+
const W = valueof(data, weight);
116118
const groupFacets = [];
117119
const groupData = [];
118120
const G = firstof(Z, F, S);
@@ -122,22 +124,22 @@ function group2(xv, yv, {z, fill, stroke, domain, normalize, ...options} = {}) {
122124
const BZ = Z && setZ([]);
123125
const BF = F && setF([]);
124126
const BS = S && setS([]);
125-
let n = data.length;
127+
let n = W ? sum(W) : data.length;
126128
let i = 0;
127129
for (const facet of facets) {
128130
const groupFacet = [];
129131
for (const I of G ? grouper(facet, i => G[i]).values() : [facet]) {
130-
if (normalize === "z") n = I.length;
132+
if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length;
131133
for (const [y, fy] of sort(grouper(I, i => Y[i]), first)) {
132134
if (!ydefined(y)) continue;
133135
for (const [x, f] of sort(grouper(fy, i => X[i]), first)) {
134136
if (!xdefined(x)) continue;
135-
const l = f.length;
137+
const l = W ? sum(f, i => W[i]) : f.length;
136138
groupFacet.push(i++);
137139
groupData.push(take(data, f));
138140
BX.push(x);
139141
BY.push(y);
140-
BL.push(k ? l * k / n : l);
142+
BL.push(m ? l * m / n : l);
141143
if (Z) BZ.push(Z[f[0]]);
142144
if (F) BF.push(F[f[0]]);
143145
if (S) BS.push(S[f[0]]);

test/data/fruit-sales.csv

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
date,fruit,units
2+
2021-03-15,apples,6
3+
2021-03-15,oranges,2
4+
2021-03-15,grapes,3
5+
2021-03-16,apples,14
6+
2021-03-16,oranges,7
7+
2021-03-16,grapes,7
8+
2021-03-16,bananas,4

test/output/fruitSales.svg

Lines changed: 60 additions & 0 deletions
Loading

test/plots/fruit-sales.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function() {
5+
const sales = await d3.csv("data/fruit-sales.csv", d3.autoType);
6+
return Plot.plot({
7+
marginLeft: 50,
8+
y: {
9+
label: null
10+
},
11+
marks: [
12+
Plot.barX(sales, Plot.groupY({y: "fruit", weight: "units"})),
13+
Plot.ruleX([0])
14+
]
15+
});
16+
}

test/plots/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {default as d3Survey2015Why} from "./d3-survey-2015-why.js";
1616
export {default as diamondsCaratPrice} from "./diamonds-carat-price.js";
1717
export {default as diamondsCaratPriceDots} from "./diamonds-carat-price-dots.js";
1818
export {default as empty} from "./empty.js";
19+
export {default as fruitSales} from "./fruit-sales.js";
1920
export {default as gistempAnomaly} from "./gistemp-anomaly.js";
2021
export {default as gistempAnomalyMoving} from "./gistemp-anomaly-moving.js";
2122
export {default as hadcrutWarmingStripes} from "./hadcrut-warming-stripes.js";

0 commit comments

Comments
 (0)