From 8ae6d9b43b3e7c5f1108dfbbe58af46326aee079 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 17:47:24 -0700 Subject: [PATCH 01/48] checkpoint new group --- src/index.js | 2 +- src/transforms/group.js | 290 +++++++++++++++-------- test/plots/moby-dick-letter-frequency.js | 2 +- 3 files changed, 196 insertions(+), 98 deletions(-) diff --git a/src/index.js b/src/index.js index 5f646f3628..119ac0566f 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js"; export {Text, text, textX, textY} from "./marks/text.js"; export {TickX, TickY, tickX, tickY} from "./marks/tick.js"; export {bin, binX, binY, binXMid, binYMid, binR} from "./transforms/bin.js"; -export {group, groupX, groupY, groupR, groupZ, groupZX, groupZY, groupZR} from "./transforms/group.js"; +export {group, groupX, groupY, groupZ} from "./transforms/group.js"; export {normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; export {reduce, reduceX, reduceY} from "./transforms/reduce.js"; diff --git a/src/transforms/group.js b/src/transforms/group.js index 5f89f41ff0..05cbc3544b 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,115 +1,155 @@ import {group as grouper, sort, sum, InternSet} from "d3"; -import {defined, firstof} from "../defined.js"; -import {valueof, maybeColor, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof} from "../mark.js"; +import {defined} from "../defined.js"; +import {valueof, maybeZ, maybeInput, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof} from "../mark.js"; // Group on {z, fill, stroke}. -export function groupZ({out = "fill", ...options} = {}) { - const [transform, L] = group2(null, null, options); - return {...transform, [out]: L}; -} - -export function groupZX(options) { - return groupZ({...options, out: "x"}); -} - -export function groupZY(options) { - return groupZ({...options, out: "y"}); -} - -export function groupZR(options) { - return groupZ({...options, out: "r"}); +export function groupZ({x, y, ...options} = {}) { + return { + ...x !== undefined && {x}, + ...y !== undefined && {y}, + ...groupn(outputs, options) + }; } // Group on {z, fill, stroke}, then on x (optionally). -export function groupX({x = identity, out = "y", ...options} = {}) { - const [transform, L, X] = group2(x, null, options); - return {...transform, x: X, [out]: L}; +export function groupX(outputs, {x = identity, y, ...options} = {}) { + return { + ...y !== undefined && {y}, + ...groupn(outputs, {x, ...options}) + }; } // Group on {z, fill, stroke}, then on y (optionally). -export function groupY({y = identity, out = "x", ...options} = {}) { - const [transform, L,, Y] = group2(null, y, options); - return {...transform, y: Y, [out]: L}; +export function groupY(outputs, {x, y = identity, ...options} = {}) { + return { + ...x !== undefined && {x}, + ...groupn(outputs, {y, ...options}) + }; } // Group on {z, fill, stroke}, then on x and y (optionally). -export function group({x, y, out = "fill", ...options} = {}) { +export function group(outputs, {x, y, ...options} = {}) { ([x, y] = maybeTuple(x, y)); - const [transform, L, X, Y] = group2(x, y, options); - return {...transform, x: X, y: Y, [out]: L}; + return groupn(outputs, {x, y, ...options}); } -export function groupR(options) { - return group({...options, out: "r"}); -} +function groupn( + { + data: reduceData = reduceIdentity, + ...outputs + } = {}, // channels to aggregate + { + x, // optionally group on x (either a value or {value, domain}) + y, // optionally group on y (either a value or {value, domain}) + domain, + // normalize, TODO + // weight, TODO + ...options + } = {} +) { -function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} = {}) { - let {value: x, domain: xdomain} = {domain, ...maybeValue(xv)}; - let {value: y, domain: ydomain} = {domain, ...maybeValue(yv)}; + // Implicit firsts. + for (const key of ["z", "fill", "stroke"]) { + if (outputs[key] === undefined && options[key] != null) { + outputs = {...outputs, [key]: reduceFirst}; + } + } + + // Reconstitute the outputs. + outputs = Object.entries(outputs).filter(([, reduce]) => reduce != null).map(([name, reduce]) => { + // const input = maybeInput(name, options); + // if (input == null) throw new Error(`missing channel: ${name}`); + const [output, setOutput] = lazyChannel(); + const r = maybeReduce(reduce); + let O; + return { + name, + output, + initialize(data) { + // O = valueof(data, input); + O = setOutput([]); + }, + reduce(I, data) { + O.push(r.reduce(I, data)); // TODO configurable channel, e.g., min(x) + } + }; + }); + + // Handle per-dimension domains. + // TODO This should be derived from the scale’s domain instead. + let xdomain, ydomain; + ({value: x, domain: xdomain} = {domain, ...maybeValue(x)}); + ({value: y, domain: ydomain} = {domain, ...maybeValue(y)}); + + // Handle both x and y being undefined. + // TODO Move to group? Needs to handle per-dimension domain with default. ([x, y] = maybeTuple(x, y)); - const m = maybeNormalize(normalize); - const [BL, setBL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); + + // Determine the z dimension (subgroups within x and y), if any. Note that + // this requires that the z dimension be defined deterministically. + const z = maybeZ(options); + + // const m = maybeNormalize(normalize); // TODO + // const [BL, setBL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); const [BX, setBX] = maybeLazyChannel(x); const [BY, setBY] = maybeLazyChannel(y); - const [BZ, setBZ] = maybeLazyChannel(z); - const [vfill] = maybeColor(fill); - const [vstroke] = maybeColor(stroke); - const [BF = fill, setBF] = maybeLazyChannel(vfill); - const [BS = stroke, setBS] = maybeLazyChannel(vstroke); + // const [BZ, setBZ] = maybeLazyChannel(z); + // const [vfill] = maybeColor(fill); + // const [vstroke] = maybeColor(stroke); + // const [BF = fill, setBF] = maybeLazyChannel(vfill); + // const [BS = stroke, setBS] = maybeLazyChannel(vstroke); const xdefined = BX && maybeDomain(xdomain); const ydefined = BY && maybeDomain(ydomain); - return [ - { - z: BZ, - fill: BF, - stroke: BS, - ...options, - transform: maybeTransform(options, (data, facets) => { - const X = valueof(data, x); - const Y = valueof(data, y); - const Z = valueof(data, z); - const F = valueof(data, vfill); - const S = valueof(data, vstroke); - const W = valueof(data, weight); - const groupFacets = []; - const groupData = []; - const G = firstof(Z, F, S); - const BL = setBL([]); - const BX = X && setBX([]); - const BY = Y && setBY([]); - const BZ = Z && setBZ([]); - const BF = F && setBF([]); - const BS = S && setBS([]); - let n = W ? sum(W) : data.length; - let i = 0; - for (const facet of facets) { - const groupFacet = []; - if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; - for (const [, I] of groups(facet, G, defined1)) { - if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; - for (const [y, fy] of groups(I, Y, ydefined)) { - for (const [x, f] of groups(fy, X, xdefined)) { - const l = W ? sum(f, i => W[i]) : f.length; - groupFacet.push(i++); - groupData.push(take(data, f)); - BL.push(m ? l * m / n : l); - if (X) BX.push(x); - if (Y) BY.push(y); - if (Z) BZ.push(Z[f[0]]); - if (F) BF.push(F[f[0]]); - if (S) BS.push(S[f[0]]); + return { + ...BX && {x: BX}, + ...BY && {y: BY}, + ...options, + ...Object.fromEntries(outputs.map(({name, output}) => [name, output])), + transform: maybeTransform(options, (data, facets) => { + const X = valueof(data, x); + const Y = valueof(data, y); + const Z = valueof(data, z); + // const W = valueof(data, weight); // TODO + const groupFacets = []; + const groupData = []; + // const BL = setBL([]); + const BX = X && setBX([]); + const BY = Y && setBY([]); + // const BZ = Z && setBZ([]); + // const BF = F && setBF([]); + // const BS = S && setBS([]); + // let n = W ? sum(W) : data.length; // TODO + let i = 0; + for (const output of outputs) { + output.initialize(data); + } + for (const facet of facets) { + const groupFacet = []; + // if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; // TODO + for (const [, I] of groups(facet, Z, defined1)) { + // if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; // TODO + for (const [y, fy] of groups(I, Y, ydefined)) { + for (const [x, f] of groups(fy, X, xdefined)) { + // const l = W ? sum(f, i => W[i]) : f.length; // TODO + groupFacet.push(i++); + groupData.push(reduceData.reduce(f, data)); + // BL.push(m ? l * m / n : l); + if (X) BX.push(x); + if (Y) BY.push(y); + for (const output of outputs) { + output.reduce(f, data); } + // if (Z) BZ.push(Z[f[0]]); + // if (F) BF.push(F[f[0]]); + // if (S) BS.push(S[f[0]]); } } - groupFacets.push(groupFacet); } - return {data: groupData, facets: groupFacets}; - }) - }, - BL, - BX, - BY - ]; + groupFacets.push(groupFacet); + } + return {data: groupData, facets: groupFacets}; + }) + }; } function maybeDomain(domain) { @@ -119,15 +159,15 @@ function maybeDomain(domain) { return ([key]) => domain.has(key); } -function maybeNormalize(normalize) { - if (!normalize) return; - if (normalize === true) return 100; - if (typeof normalize === "number") return normalize; - switch ((normalize + "").toLowerCase()) { - case "facet": case "z": return 100; - } - throw new Error("invalid normalize"); -} +// function maybeNormalize(normalize) { +// if (!normalize) return; +// if (normalize === true) return 100; +// if (typeof normalize === "number") return normalize; +// switch ((normalize + "").toLowerCase()) { +// case "facet": case "z": return 100; +// } +// throw new Error("invalid normalize"); +// } function defined1([key]) { return defined(key); @@ -136,3 +176,61 @@ function defined1([key]) { export function groups(I, X, defined = defined1) { return X ? sort(grouper(I, i => X[i]), first).filter(defined) : [[, I]]; } + +function maybeReduce(reduce) { + if (reduce && typeof reduce.reduce === "function") return reduce; + if (typeof reduce === "function") return reduceFunction(reduce); + switch ((reduce + "").toLowerCase()) { + case "first": return reduceFirst; + case "last": return reduceLast; + case "count": return reduceCount; // TODO normalized proportion + case "deviation": return reduceAccessor(deviation); + case "min": return reduceAccessor(min); + case "max": return reduceAccessor(max); + case "mean": return reduceAccessor(mean); + case "median": return reduceAccessor(median); + case "sum": return reduceAccessor(sum); + case "variance": return reduceAccessor(variance); + } + throw new Error("invalid reduce"); +} + +function reduceFunction(f) { + return { + reduce(I, X) { + return f(take(X, I)); + } + }; +} + +function reduceAccessor(f) { + return { + reduce(I, X) { + return f(I, i => X[i]); + } + }; +} + +const reduceIdentity = { + reduce(I, X) { + return take(X, I); + } +}; + +const reduceFirst = { + reduce(I, X) { + return X[I[0]]; + } +}; + +const reduceLast = { + reduce(I, X) { + return X[I[I.length - 1]]; + } +}; + +const reduceCount = { + reduce(I) { + return I.length; + } +}; diff --git a/test/plots/moby-dick-letter-frequency.js b/test/plots/moby-dick-letter-frequency.js index a531036f2b..4228e01824 100644 --- a/test/plots/moby-dick-letter-frequency.js +++ b/test/plots/moby-dick-letter-frequency.js @@ -9,7 +9,7 @@ export default async function() { grid: true }, marks: [ - Plot.barY(letters, Plot.groupX()), + Plot.barY(letters, Plot.groupX({y: "count"})), Plot.ruleY([0]) ] }); From 82f9457a27386397ed294ea66782d644878e3d11 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 18:49:49 -0700 Subject: [PATCH 02/48] group boxplot! --- src/transforms/group.js | 75 ++++++++++++++++-------------------- test/plots/morley-boxplot.js | 6 +-- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 05cbc3544b..ed6bd0db00 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,46 +1,39 @@ import {group as grouper, sort, sum, InternSet} from "d3"; import {defined} from "../defined.js"; -import {valueof, maybeZ, maybeInput, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof} from "../mark.js"; +import {valueof, maybeZ, maybeInput, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof, maybeColor} from "../mark.js"; // Group on {z, fill, stroke}. -export function groupZ({x, y, ...options} = {}) { - return { - ...x !== undefined && {x}, - ...y !== undefined && {y}, - ...groupn(outputs, options) - }; +export function groupZ(outputs, options) { + return groupn(null, null, outputs, options); } // Group on {z, fill, stroke}, then on x (optionally). -export function groupX(outputs, {x = identity, y, ...options} = {}) { - return { - ...y !== undefined && {y}, - ...groupn(outputs, {x, ...options}) - }; +export function groupX(outputs, options = {}) { + const {x = identity} = options; + return groupn(x, null, outputs, options); } // Group on {z, fill, stroke}, then on y (optionally). -export function groupY(outputs, {x, y = identity, ...options} = {}) { - return { - ...x !== undefined && {x}, - ...groupn(outputs, {y, ...options}) - }; +export function groupY(outputs, options = {}) { + const {y = identity} = options; + return groupn(null, y, outputs, options); } // Group on {z, fill, stroke}, then on x and y (optionally). -export function group(outputs, {x, y, ...options} = {}) { +export function group(outputs, options = {}) { + let {x, y} = options; ([x, y] = maybeTuple(x, y)); - return groupn(outputs, {x, y, ...options}); + return groupn(x, y, outputs, options); } function groupn( + x, // optionally group on x (either a value or {value, domain}) + y, // optionally group on y (either a value or {value, domain}) { data: reduceData = reduceIdentity, ...outputs } = {}, // channels to aggregate { - x, // optionally group on x (either a value or {value, domain}) - y, // optionally group on y (either a value or {value, domain}) domain, // normalize, TODO // weight, TODO @@ -49,41 +42,39 @@ function groupn( ) { // Implicit firsts. - for (const key of ["z", "fill", "stroke"]) { - if (outputs[key] === undefined && options[key] != null) { - outputs = {...outputs, [key]: reduceFirst}; - } - } + if (outputs.z === undefined && options.z != null) outputs = {...outputs, z: reduceFirst}; + if (outputs.fill === undefined && maybeColor(options.fill)[0]) outputs = {...outputs, fill: reduceFirst}; + if (outputs.stroke === undefined && maybeColor(options.stroke)[0]) outputs = {...outputs, stroke: reduceFirst}; // Reconstitute the outputs. outputs = Object.entries(outputs).filter(([, reduce]) => reduce != null).map(([name, reduce]) => { - // const input = maybeInput(name, options); - // if (input == null) throw new Error(`missing channel: ${name}`); - const [output, setOutput] = lazyChannel(); - const r = maybeReduce(reduce); - let O; + const reducer = maybeReduce(reduce); + const value = maybeInput(name, options); + if (value == null && reducer !== reduceCount) throw new Error(`missing channel: ${name}`); + const [output, setOutput] = lazyChannel(value); + let V, O; return { name, output, initialize(data) { - // O = valueof(data, input); + V = valueof(data, value); O = setOutput([]); }, - reduce(I, data) { - O.push(r.reduce(I, data)); // TODO configurable channel, e.g., min(x) + reduce(I) { + O.push(reducer.reduce(I, V)); } }; }); // Handle per-dimension domains. // TODO This should be derived from the scale’s domain instead. - let xdomain, ydomain; - ({value: x, domain: xdomain} = {domain, ...maybeValue(x)}); - ({value: y, domain: ydomain} = {domain, ...maybeValue(y)}); + // let xdomain, ydomain; + // ({value: x, domain: xdomain} = {domain, ...maybeValue(x)}); + // ({value: y, domain: ydomain} = {domain, ...maybeValue(y)}); // Handle both x and y being undefined. // TODO Move to group? Needs to handle per-dimension domain with default. - ([x, y] = maybeTuple(x, y)); + // ([x, y] = maybeTuple(x, y)); // Determine the z dimension (subgroups within x and y), if any. Note that // this requires that the z dimension be defined deterministically. @@ -98,12 +89,12 @@ function groupn( // const [vstroke] = maybeColor(stroke); // const [BF = fill, setBF] = maybeLazyChannel(vfill); // const [BS = stroke, setBS] = maybeLazyChannel(vstroke); - const xdefined = BX && maybeDomain(xdomain); - const ydefined = BY && maybeDomain(ydomain); + const xdefined = defined1; // TODO BX && maybeDomain(xdomain); + const ydefined = defined1; // TODO BY && maybeDomain(ydomain); return { + ...options, ...BX && {x: BX}, ...BY && {y: BY}, - ...options, ...Object.fromEntries(outputs.map(({name, output}) => [name, output])), transform: maybeTransform(options, (data, facets) => { const X = valueof(data, x); @@ -137,7 +128,7 @@ function groupn( if (X) BX.push(x); if (Y) BY.push(y); for (const output of outputs) { - output.reduce(f, data); + output.reduce(f); } // if (Z) BZ.push(Z[f[0]]); // if (F) BF.push(F[f[0]]); diff --git a/test/plots/morley-boxplot.js b/test/plots/morley-boxplot.js index 9b2f2f5351..fdd5819dc4 100644 --- a/test/plots/morley-boxplot.js +++ b/test/plots/morley-boxplot.js @@ -24,9 +24,9 @@ function boxX(data, { ...options } = {}) { return [ - Plot.ruleY(data, Plot.reduceX({x1: iqr1, x2: iqr2}, {x, y, stroke, ...options})), - Plot.barX(data, Plot.reduceX({x1: quartile1, x2: quartile3}, {x, y, fill, ...options})), - Plot.tickX(data, Plot.reduceX({x: median}, {x, y, stroke, strokeWidth: 2, ...options})), + Plot.ruleY(data, Plot.groupY({x1: iqr1, x2: iqr2}, {x, y, stroke, ...options})), + Plot.barX(data, Plot.groupY({x1: quartile1, x2: quartile3}, {x, y, fill, ...options})), + Plot.tickX(data, Plot.groupY({x: median}, {x, y, stroke, strokeWidth: 2, ...options})), Plot.dot(data, Plot.map({x: outliers}, {x, y, z: y, stroke, ...options})) ]; } From 4dad4a061815748acbcf2f439fdd4371bd734e0f Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 18:52:45 -0700 Subject: [PATCH 03/48] check for value-less reduce --- src/transforms/group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index ed6bd0db00..62c921e646 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -50,7 +50,7 @@ function groupn( outputs = Object.entries(outputs).filter(([, reduce]) => reduce != null).map(([name, reduce]) => { const reducer = maybeReduce(reduce); const value = maybeInput(name, options); - if (value == null && reducer !== reduceCount) throw new Error(`missing channel: ${name}`); + if (value == null && reducer.reduce.length > 1) throw new Error(`missing channel: ${name}`); const [output, setOutput] = lazyChannel(value); let V, O; return { From 73fcb9306f192b85a0c6165ce61b70d7866e7000 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 18:59:21 -0700 Subject: [PATCH 04/48] update example --- test/plots/fruit-sales.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plots/fruit-sales.js b/test/plots/fruit-sales.js index 56139f3efa..c26feed074 100644 --- a/test/plots/fruit-sales.js +++ b/test/plots/fruit-sales.js @@ -9,7 +9,7 @@ export default async function() { label: null }, marks: [ - Plot.barX(sales, Plot.groupY({y: "fruit", weight: "units"})), + Plot.barX(sales, Plot.groupY({x: "sum"}, {x: "units", y: "fruit"})), Plot.ruleX([0]) ] }); From e578c54c3a8421654f9c8670d053bea6b28611f4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 19:03:13 -0700 Subject: [PATCH 05/48] require reduce --- src/transforms/group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 62c921e646..06e5f6ffa4 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -47,7 +47,7 @@ function groupn( if (outputs.stroke === undefined && maybeColor(options.stroke)[0]) outputs = {...outputs, stroke: reduceFirst}; // Reconstitute the outputs. - outputs = Object.entries(outputs).filter(([, reduce]) => reduce != null).map(([name, reduce]) => { + outputs = Object.entries(outputs).map(([name, reduce]) => { const reducer = maybeReduce(reduce); const value = maybeInput(name, options); if (value == null && reducer.reduce.length > 1) throw new Error(`missing channel: ${name}`); From a81ac420aaeedce5253d8479bdde2f2b347ca42c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 19:26:59 -0700 Subject: [PATCH 06/48] =?UTF-8?q?don=E2=80=99t=20compute=20z=20twice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/transforms/group.js | 82 ++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 54 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 06e5f6ffa4..5fc0cd0eee 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,6 +1,5 @@ -import {group as grouper, sort, sum, InternSet} from "d3"; -import {defined} from "../defined.js"; -import {valueof, maybeZ, maybeInput, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof, maybeColor} from "../mark.js"; +import {group as grouper, sort, sum} from "d3"; +import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take} from "../mark.js"; // Group on {z, fill, stroke}. export function groupZ(outputs, options) { @@ -27,8 +26,8 @@ export function group(outputs, options = {}) { } function groupn( - x, // optionally group on x (either a value or {value, domain}) - y, // optionally group on y (either a value or {value, domain}) + x, // optionally group on x + y, // optionally group on y { data: reduceData = reduceIdentity, ...outputs @@ -36,16 +35,13 @@ function groupn( { domain, // normalize, TODO - // weight, TODO + z, + fill, + stroke, ...options } = {} ) { - // Implicit firsts. - if (outputs.z === undefined && options.z != null) outputs = {...outputs, z: reduceFirst}; - if (outputs.fill === undefined && maybeColor(options.fill)[0]) outputs = {...outputs, fill: reduceFirst}; - if (outputs.stroke === undefined && maybeColor(options.stroke)[0]) outputs = {...outputs, stroke: reduceFirst}; - // Reconstitute the outputs. outputs = Object.entries(outputs).map(([name, reduce]) => { const reducer = maybeReduce(reduce); @@ -66,32 +62,19 @@ function groupn( }; }); - // Handle per-dimension domains. - // TODO This should be derived from the scale’s domain instead. - // let xdomain, ydomain; - // ({value: x, domain: xdomain} = {domain, ...maybeValue(x)}); - // ({value: y, domain: ydomain} = {domain, ...maybeValue(y)}); - - // Handle both x and y being undefined. - // TODO Move to group? Needs to handle per-dimension domain with default. - // ([x, y] = maybeTuple(x, y)); - - // Determine the z dimension (subgroups within x and y), if any. Note that - // this requires that the z dimension be defined deterministically. - const z = maybeZ(options); - // const m = maybeNormalize(normalize); // TODO // const [BL, setBL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); const [BX, setBX] = maybeLazyChannel(x); const [BY, setBY] = maybeLazyChannel(y); - // const [BZ, setBZ] = maybeLazyChannel(z); - // const [vfill] = maybeColor(fill); - // const [vstroke] = maybeColor(stroke); - // const [BF = fill, setBF] = maybeLazyChannel(vfill); - // const [BS = stroke, setBS] = maybeLazyChannel(vstroke); - const xdefined = defined1; // TODO BX && maybeDomain(xdomain); - const ydefined = defined1; // TODO BY && maybeDomain(ydomain); + const [BZ, setBZ] = maybeLazyChannel(z); + const [vfill] = maybeColor(fill); + const [vstroke] = maybeColor(stroke); + const [BF = fill, setBF] = maybeLazyChannel(vfill); + const [BS = stroke, setBS] = maybeLazyChannel(vstroke); return { + z: BZ, + fill: BF, + stroke: BS, ...options, ...BX && {x: BX}, ...BY && {y: BY}, @@ -100,15 +83,17 @@ function groupn( const X = valueof(data, x); const Y = valueof(data, y); const Z = valueof(data, z); + const F = valueof(data, vfill); + const S = valueof(data, vstroke); // const W = valueof(data, weight); // TODO const groupFacets = []; const groupData = []; // const BL = setBL([]); const BX = X && setBX([]); const BY = Y && setBY([]); - // const BZ = Z && setBZ([]); - // const BF = F && setBF([]); - // const BS = S && setBS([]); + const BZ = Z && setBZ([]); + const BF = F && setBF([]); + const BS = S && setBS([]); // let n = W ? sum(W) : data.length; // TODO let i = 0; for (const output of outputs) { @@ -117,10 +102,10 @@ function groupn( for (const facet of facets) { const groupFacet = []; // if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; // TODO - for (const [, I] of groups(facet, Z, defined1)) { + for (const [, I] of groups(facet, Z)) { // if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; // TODO - for (const [y, fy] of groups(I, Y, ydefined)) { - for (const [x, f] of groups(fy, X, xdefined)) { + for (const [y, fy] of groups(I, Y)) { + for (const [x, f] of groups(fy, X)) { // const l = W ? sum(f, i => W[i]) : f.length; // TODO groupFacet.push(i++); groupData.push(reduceData.reduce(f, data)); @@ -130,9 +115,9 @@ function groupn( for (const output of outputs) { output.reduce(f); } - // if (Z) BZ.push(Z[f[0]]); - // if (F) BF.push(F[f[0]]); - // if (S) BS.push(S[f[0]]); + if (Z) BZ.push(Z[f[0]]); + if (F) BF.push(F[f[0]]); + if (S) BS.push(S[f[0]]); } } } @@ -143,13 +128,6 @@ function groupn( }; } -function maybeDomain(domain) { - if (domain === undefined) return defined1; - if (domain === null) return () => false; - domain = new InternSet(domain); - return ([key]) => domain.has(key); -} - // function maybeNormalize(normalize) { // if (!normalize) return; // if (normalize === true) return 100; @@ -160,12 +138,8 @@ function maybeDomain(domain) { // throw new Error("invalid normalize"); // } -function defined1([key]) { - return defined(key); -} - -export function groups(I, X, defined = defined1) { - return X ? sort(grouper(I, i => X[i]), first).filter(defined) : [[, I]]; +export function groups(I, X) { + return X ? sort(grouper(I, i => X[i]), first) : [[, I]]; } function maybeReduce(reduce) { From 75e0b870dfa61d5f003d2c05910fada3906c36e7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 19:27:38 -0700 Subject: [PATCH 07/48] reorder --- src/transforms/group.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 5fc0cd0eee..e756e493ff 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -112,12 +112,12 @@ function groupn( // BL.push(m ? l * m / n : l); if (X) BX.push(x); if (Y) BY.push(y); - for (const output of outputs) { - output.reduce(f); - } if (Z) BZ.push(Z[f[0]]); if (F) BF.push(F[f[0]]); if (S) BS.push(S[f[0]]); + for (const output of outputs) { + output.reduce(f); + } } } } From c2f3af6184674fdafd3016df957bda4f9b089206 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 19:28:58 -0700 Subject: [PATCH 08/48] missing imports --- src/transforms/group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index e756e493ff..a1fe6a8081 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,4 +1,4 @@ -import {group as grouper, sort, sum} from "d3"; +import {group as grouper, sort, sum, deviation, min, max, mean, median, variance} from "d3"; import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take} from "../mark.js"; // Group on {z, fill, stroke}. From b80380e768b634e7885c506cb6e22622a2038a34 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 Mar 2021 19:30:37 -0700 Subject: [PATCH 09/48] bin groups --- src/index.js | 1 - src/transforms/bin.js | 2 +- src/transforms/reduce.js | 122 --------------------------------------- 3 files changed, 1 insertion(+), 124 deletions(-) delete mode 100644 src/transforms/reduce.js diff --git a/src/index.js b/src/index.js index 119ac0566f..08c993f41e 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,6 @@ export {bin, binX, binY, binXMid, binYMid, binR} from "./transforms/bin.js"; export {group, groupX, groupY, groupZ} from "./transforms/group.js"; export {normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; -export {reduce, reduceX, reduceY} from "./transforms/reduce.js"; export {windowX, windowY} from "./transforms/window.js"; export {selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY} from "./transforms/select.js"; export {stackX, stackX1, stackX2, stackXMid, stackY, stackY1, stackY2, stackYMid} from "./transforms/stack.js"; diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 0c1a11b070..2d17267a57 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -85,7 +85,7 @@ function bin1(x, key, {[key]: k, z, fill, stroke, weight, domain, thresholds, no if (cumulative < 0) B.reverse(); for (const facet of facets) { const binFacet = []; - for (const I of G ? group(facet, i => G[i]).values() : [facet]) { + for (const [, I] of groups(facet, G)) { if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; const set = new Set(I); let f; diff --git a/src/transforms/reduce.js b/src/transforms/reduce.js deleted file mode 100644 index 043e88ba2d..0000000000 --- a/src/transforms/reduce.js +++ /dev/null @@ -1,122 +0,0 @@ -import {deviation, group, min, max, mean, median, sum, variance} from "d3"; -import {firstof} from "../defined.js"; -import {lazyChannel, maybeColor, maybeLazyChannel, maybeInput, maybeTransform, take, valueof} from "../mark.js"; - -// Group on y, z, fill, or stroke, if any, then reduce. -export function reduceX(outputs, options) { - return reducen("y", outputs, options); -} - -// Group on x, z, fill, or stroke, if any, then reduce. -export function reduceY(outputs, options) { - return reducen("x", outputs, options); -} - -// Group on z, fill, or stroke, if any, then reduce. -export function reduce(outputs, options) { - return reducen(undefined, outputs, options); -} - -function reducen( - key, // an optional additional group channel (x or y, typically) - {data: reduceData = reduceIdentity, ...outputs} = {}, // channels to reduce - options = {} // channels to group, and options -) { - const {[key]: k, z, fill, stroke, ...rest} = options; - reduceData = maybeReduce(reduceData); - - // All channels that are candidates for grouping are aggregated by picking the - // first value from the corresponding input value, even if they are not - // actually used for grouping. - const [zfill] = maybeColor(fill); - const [zstroke] = maybeColor(stroke); - const [RK, setRK] = maybeLazyChannel(k); - const [RZ, setRZ] = maybeLazyChannel(z); - const [RF = fill, setRF] = maybeLazyChannel(zfill); - const [RS = stroke, setRS] = maybeLazyChannel(zstroke); - - // All output channels are aggregated by applying the corresponding specified - // reducer on the associated input values for each group. - const channels = Object.entries(outputs).map(([key, reduce]) => { - const input = maybeInput(key, options); - if (input == null) throw new Error(`missing channel: ${key}`); - const [output, setOutput] = lazyChannel(input); - return {key, input, output, setOutput, reduce: maybeReduce(reduce)}; - }); - - return { - ...key && {[key]: RK}, - z: RZ, - fill: RF, - stroke: RS, - ...rest, - ...Object.fromEntries(channels.map(({key, output}) => [key, output])), - transform: maybeTransform(options, (data, facets) => { - const outFacets = []; - const outData = []; - const X = channels.map(({input}) => valueof(data, input)); - const RX = channels.map(({setOutput}) => setOutput([])); - const K = valueof(data, k); - const Z = valueof(data, z); - const F = valueof(data, zfill); - const S = valueof(data, zstroke); - const G = firstof(K, Z, F, S); - const RK = K && setRK([]); - const RZ = Z && setRZ([]); - const RF = F && setRF([]); - const RS = S && setRS([]); - let i = 0; - for (const facet of facets) { - const outFacet = []; - for (const I of G ? group(facet, i => G[i]).values() : [facet]) { - outFacet.push(i++); - outData.push(reduceData.reduce(I, data)); - channels.forEach(({reduce}, i) => RX[i].push(reduce.reduce(I, X[i]))); - if (K) RK.push(K[I[0]]); - if (Z) RZ.push(Z[I[0]]); - if (F) RF.push(F[I[0]]); - if (S) RS.push(S[I[0]]); - } - outFacets.push(outFacet); - } - return {data: outData, facets: outFacets}; - }) - }; -} - -function maybeReduce(reduce) { - if (reduce && typeof reduce.reduce === "function") return reduce; - if (typeof reduce === "function") return reduceFunction(reduce); - switch ((reduce + "").toLowerCase()) { - case "deviation": return reduceFunction2(deviation); - case "min": return reduceFunction2(min); - case "max": return reduceFunction2(max); - case "mean": return reduceFunction2(mean); - case "median": return reduceFunction2(median); - case "sum": return reduceFunction2(sum); - case "variance": return reduceFunction2(variance); - } - throw new Error("invalid reduce"); -} - -function reduceFunction(f) { - return { - reduce(I, X) { - return f(take(X, I)); - } - }; -} - -function reduceFunction2(f) { - return { - reduce(I, X) { - return f(I, i => X[i]); - } - }; -} - -const reduceIdentity = { - reduce(I, X) { - return take(X, I); - } -}; From f0dff40b84983d4873887f23d7e78e15cbf52fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 25 Mar 2021 09:04:08 +0100 Subject: [PATCH 10/48] fix reducer test --- test/transforms/reduce-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/transforms/reduce-test.js b/test/transforms/reduce-test.js index e0caa798ea..a00d6eb36f 100644 --- a/test/transforms/reduce-test.js +++ b/test/transforms/reduce-test.js @@ -19,7 +19,7 @@ tape("function reducers reduce as expected", test => { }); function testReducer(test, data, x, r) { - const mark = Plot.dot(data, Plot.reduceX({x}, {x: d => d})); + const mark = Plot.dot(data, Plot.groupZ({x}, {x: d => d})); const c = new Map(mark.initialize().channels); test.deepEqual(c.get("x").value, [r]); } From 1978ee7662abec25a3aa3e04a3e66b043f1f50b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 25 Mar 2021 09:05:49 +0100 Subject: [PATCH 11/48] =?UTF-8?q?half-fix=20plots=20that=20need=20default?= =?UTF-8?q?=20{x:=20count}=20or=20{fill:=20count}=20still=20missing:=20-?= =?UTF-8?q?=20normalize=20-=20automatic=20scale=20label=20(Frequency=20?= =?UTF-8?q?=E2=86=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/plots/moby-dick-faceted.js | 2 +- test/plots/moby-dick-letter-position.js | 2 +- test/plots/penguin-species-group.js | 4 ++-- test/plots/penguin-species-island.js | 2 +- test/plots/seattle-temperature-cell.js | 2 +- test/plots/word-length-moby-dick.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/plots/moby-dick-faceted.js b/test/plots/moby-dick-faceted.js index 489790ff44..e405e8f654 100644 --- a/test/plots/moby-dick-faceted.js +++ b/test/plots/moby-dick-faceted.js @@ -17,7 +17,7 @@ export default async function() { y: cases }, marks: [ - Plot.barY(letters, Plot.groupX({x: uppers})), + Plot.barY(letters, Plot.groupX({y: "count"}, {x: uppers})), Plot.ruleY([0]) ] }); diff --git a/test/plots/moby-dick-letter-position.js b/test/plots/moby-dick-letter-position.js index 4352b868d0..82a2f72241 100644 --- a/test/plots/moby-dick-letter-position.js +++ b/test/plots/moby-dick-letter-position.js @@ -30,7 +30,7 @@ export default async function() { scheme: "blues" }, marks: [ - Plot.cell(positions, Plot.group({insetTop: 1, insetLeft: 1})) + Plot.cell(positions, Plot.group({fill: "count"}, {insetTop: 1, insetLeft: 1})) ] }); } diff --git a/test/plots/penguin-species-group.js b/test/plots/penguin-species-group.js index f915e9b7d9..459129e512 100644 --- a/test/plots/penguin-species-group.js +++ b/test/plots/penguin-species-group.js @@ -5,8 +5,8 @@ export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ marks: [ - Plot.barX(penguins, Plot.stackX(Plot.groupZX({fill: "species", normalize: true}))), - Plot.text(penguins, Plot.stackXMid(Plot.groupZX({z: "species", normalize: true, text: ([d]) => d.species}))), + Plot.barX(penguins, Plot.stackX(Plot.groupZ({x: "count"}, {fill: "species", normalize: true}))), + Plot.text(penguins, Plot.stackXMid(Plot.groupZ({x: "count"}, {z: "species", normalize: true, text: ([d]) => d.species}))), Plot.ruleX([0, 100]) ] }); diff --git a/test/plots/penguin-species-island.js b/test/plots/penguin-species-island.js index d06d3bb097..7851ef8094 100644 --- a/test/plots/penguin-species-island.js +++ b/test/plots/penguin-species-island.js @@ -8,7 +8,7 @@ export default async function() { grid: true }, marks: [ - Plot.barY(data, Plot.stackY(Plot.groupX({x: "species", fill: "island"}))), + Plot.barY(data, Plot.stackY(Plot.groupX({y: "count"}, {x: "species", fill: "island", z: "island"}))), Plot.ruleY([0]) ] }); diff --git a/test/plots/seattle-temperature-cell.js b/test/plots/seattle-temperature-cell.js index 43568ae5fd..aec1476db8 100644 --- a/test/plots/seattle-temperature-cell.js +++ b/test/plots/seattle-temperature-cell.js @@ -11,7 +11,7 @@ export default async function() { }, marks: [ Plot.cell(seattle, { - ...Plot.group({ + ...Plot.group({fill: "count"}, { x: d => d.date.getUTCDate(), y: d => d.date.getUTCMonth() }), diff --git a/test/plots/word-length-moby-dick.js b/test/plots/word-length-moby-dick.js index 9eaf23e7ae..77c6167ec9 100644 --- a/test/plots/word-length-moby-dick.js +++ b/test/plots/word-length-moby-dick.js @@ -20,7 +20,7 @@ export default async function() { grid: true }, marks: [ - Plot.barY(words, Plot.groupX({x: d => d.length, normalize: true})) + Plot.barY(words, Plot.groupX({y: "count"}, {x: d => d.length, normalize: true})) ] }); } From 99de814f1db23b62dcefb997b54bb6f127cff51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 25 Mar 2021 09:06:12 +0100 Subject: [PATCH 12/48] fix plots (reduce => group) --- test/plots/simpsons-ratings-dots.js | 4 ++-- test/plots/us-population-state-age-dots.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/plots/simpsons-ratings-dots.js b/test/plots/simpsons-ratings-dots.js index 947f0505ef..a2eef52bb7 100644 --- a/test/plots/simpsons-ratings-dots.js +++ b/test/plots/simpsons-ratings-dots.js @@ -13,8 +13,8 @@ export default async function() { label: "↑ IMDb rating" }, marks: [ - Plot.ruleX(simpsons, Plot.reduceY({y1: "min", y2: "max"}, {x: "season", y: "imdb_rating"})), - Plot.line(simpsons, Plot.reduceY({y: "median"}, {x: "season", y: "imdb_rating", stroke: "red"})), + Plot.ruleX(simpsons, Plot.groupX({y1: "min", y2: "max"}, {x: "season", y: "imdb_rating"})), + Plot.line(simpsons, Plot.groupX({y: "median"}, {x: "season", y: "imdb_rating", stroke: "red"})), Plot.dot(simpsons, {x: "season", y: "imdb_rating"}) ] }); diff --git a/test/plots/us-population-state-age-dots.js b/test/plots/us-population-state-age-dots.js index ca61438c01..af3e894370 100644 --- a/test/plots/us-population-state-age-dots.js +++ b/test/plots/us-population-state-age-dots.js @@ -24,7 +24,7 @@ export default async function() { }, marks: [ Plot.ruleX([0]), - Plot.ruleY(stateage, Plot.reduceX({x1: "min", x2: "max"}, position)), + Plot.ruleY(stateage, Plot.groupY({x1: "min", x2: "max"}, position)), Plot.dot(stateage, {...position, fill: "age"}), Plot.text(stateage, Plot.selectMinX({...position, textAnchor: "end", dx: -6, text: "state"})) ] From 43fbf3a6469b7bd2715c0e2ce3347f29512445ce Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 10:00:59 -0700 Subject: [PATCH 13/48] firstof {z, fill, stroke} --- src/transforms/group.js | 12 ++++++------ test/plots/moby-dick-letter-position.js | 2 +- test/plots/penguin-species-island.js | 2 +- test/plots/seattle-temperature-cell.js | 12 +++++------- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index a1fe6a8081..4fbf806510 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,4 +1,5 @@ import {group as grouper, sort, sum, deviation, min, max, mean, median, variance} from "d3"; +import {firstof} from "../defined.js"; import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take} from "../mark.js"; // Group on {z, fill, stroke}. @@ -35,17 +36,15 @@ function groupn( { domain, // normalize, TODO - z, - fill, - stroke, - ...options + ...inputs } = {} ) { + const {z, fill, stroke, ...options} = inputs; // Reconstitute the outputs. outputs = Object.entries(outputs).map(([name, reduce]) => { const reducer = maybeReduce(reduce); - const value = maybeInput(name, options); + const value = maybeInput(name, inputs); if (value == null && reducer.reduce.length > 1) throw new Error(`missing channel: ${name}`); const [output, setOutput] = lazyChannel(value); let V, O; @@ -85,6 +84,7 @@ function groupn( const Z = valueof(data, z); const F = valueof(data, vfill); const S = valueof(data, vstroke); + const G = firstof(Z, F, S); // const W = valueof(data, weight); // TODO const groupFacets = []; const groupData = []; @@ -102,7 +102,7 @@ function groupn( for (const facet of facets) { const groupFacet = []; // if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; // TODO - for (const [, I] of groups(facet, Z)) { + for (const [, I] of groups(facet, G)) { // if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; // TODO for (const [y, fy] of groups(I, Y)) { for (const [x, f] of groups(fy, X)) { diff --git a/test/plots/moby-dick-letter-position.js b/test/plots/moby-dick-letter-position.js index 82a2f72241..58829d26c9 100644 --- a/test/plots/moby-dick-letter-position.js +++ b/test/plots/moby-dick-letter-position.js @@ -30,7 +30,7 @@ export default async function() { scheme: "blues" }, marks: [ - Plot.cell(positions, Plot.group({fill: "count"}, {insetTop: 1, insetLeft: 1})) + Plot.cell(positions, Plot.group({fill: "count"}, {inset: 0.5})) ] }); } diff --git a/test/plots/penguin-species-island.js b/test/plots/penguin-species-island.js index 7851ef8094..6669a1e8cd 100644 --- a/test/plots/penguin-species-island.js +++ b/test/plots/penguin-species-island.js @@ -8,7 +8,7 @@ export default async function() { grid: true }, marks: [ - Plot.barY(data, Plot.stackY(Plot.groupX({y: "count"}, {x: "species", fill: "island", z: "island"}))), + Plot.barY(data, Plot.stackY(Plot.groupX({y: "count"}, {x: "species", fill: "island"}))), Plot.ruleY([0]) ] }); diff --git a/test/plots/seattle-temperature-cell.js b/test/plots/seattle-temperature-cell.js index aec1476db8..4e0d00e98c 100644 --- a/test/plots/seattle-temperature-cell.js +++ b/test/plots/seattle-temperature-cell.js @@ -10,14 +10,12 @@ export default async function() { tickFormat: i => "JFMAMJJASOND"[i] }, marks: [ - Plot.cell(seattle, { - ...Plot.group({fill: "count"}, { - x: d => d.date.getUTCDate(), - y: d => d.date.getUTCMonth() - }), - fill: d => d3.max(d, d => d.temp_max), + Plot.cell(seattle, Plot.group({fill: "max"}, { + x: d => d.date.getUTCDate(), + y: d => d.date.getUTCMonth(), + fill: "temp_max", inset: 0.5 - }) + })) ] }); } From 73fcb56a8500ad54ff39b21a06b806bb0b104628 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 10:21:09 -0700 Subject: [PATCH 14/48] reduce proportion --- src/transforms/group.js | 40 ++++++++++++++++------------- test/plots/word-length-moby-dick.js | 6 +++-- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 4fbf806510..b29f28597d 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -29,23 +29,15 @@ export function group(outputs, options = {}) { function groupn( x, // optionally group on x y, // optionally group on y - { - data: reduceData = reduceIdentity, - ...outputs - } = {}, // channels to aggregate - { - domain, - // normalize, TODO - ...inputs - } = {} + {data: reduceData = reduceIdentity, ...outputs} = {}, // output channel definitions + {domain, ...inputs} = {} // input channels and options ) { - const {z, fill, stroke, ...options} = inputs; - // Reconstitute the outputs. + // Prepare the output channels: detect the corresponding inputs and reducers. outputs = Object.entries(outputs).map(([name, reduce]) => { const reducer = maybeReduce(reduce); - const value = maybeInput(name, inputs); - if (value == null && reducer.reduce.length > 1) throw new Error(`missing channel: ${name}`); + const value = reducer === reduceCount || reducer === reduceProportion ? identity : maybeInput(name, inputs); // TODO + if (value == null) throw new Error(`missing channel: ${name}`); const [output, setOutput] = lazyChannel(value); let V, O; return { @@ -61,15 +53,22 @@ function groupn( }; }); - // const m = maybeNormalize(normalize); // TODO - // const [BL, setBL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); + // The x and y channels are used for grouping. Note that the passed-through + // options may also include x and y channels which are ignored, so we only + // want to generate these as output channels if they were used for grouping. const [BX, setBX] = maybeLazyChannel(x); const [BY, setBY] = maybeLazyChannel(y); + + // The z, fill, and stroke channels (if channels and not constants) are + // greedily materialized by the transform so that we can reference them for + // subdividing groups without having to compute them more than once. + const {z, fill, stroke, ...options} = inputs; const [BZ, setBZ] = maybeLazyChannel(z); const [vfill] = maybeColor(fill); const [vstroke] = maybeColor(stroke); const [BF = fill, setBF] = maybeLazyChannel(vfill); const [BS = stroke, setBS] = maybeLazyChannel(vstroke); + return { z: BZ, fill: BF, @@ -85,10 +84,8 @@ function groupn( const F = valueof(data, vfill); const S = valueof(data, vstroke); const G = firstof(Z, F, S); - // const W = valueof(data, weight); // TODO const groupFacets = []; const groupData = []; - // const BL = setBL([]); const BX = X && setBX([]); const BY = Y && setBY([]); const BZ = Z && setBZ([]); @@ -148,7 +145,8 @@ function maybeReduce(reduce) { switch ((reduce + "").toLowerCase()) { case "first": return reduceFirst; case "last": return reduceLast; - case "count": return reduceCount; // TODO normalized proportion + case "count": return reduceCount; + case "proportion": return reduceProportion; case "deviation": return reduceAccessor(deviation); case "min": return reduceAccessor(min); case "max": return reduceAccessor(max); @@ -199,3 +197,9 @@ const reduceCount = { return I.length; } }; + +const reduceProportion = { + reduce(I, X) { + return I.length / X.length; + } +}; diff --git a/test/plots/word-length-moby-dick.js b/test/plots/word-length-moby-dick.js index 77c6167ec9..09a3d04103 100644 --- a/test/plots/word-length-moby-dick.js +++ b/test/plots/word-length-moby-dick.js @@ -17,10 +17,12 @@ export default async function() { labelAnchor: "right" }, y: { - grid: true + grid: true, + transform: d => d * 100, + label: "↑ Frequency (%)" }, marks: [ - Plot.barY(words, Plot.groupX({y: "count"}, {x: d => d.length, normalize: true})) + Plot.barY(words, Plot.groupX({y: "proportion"}, {x: d => d.length})) ] }); } From ffd3b4715764d04b826ad6417ea6feed91d1fd93 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 10:30:44 -0700 Subject: [PATCH 15/48] maybeGroup --- src/transforms/bin.js | 6 +++--- src/transforms/group.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 2d17267a57..d707c7ea08 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -2,7 +2,7 @@ import {bin as binner, cross, group, sum} from "d3"; import {firstof} from "../defined.js"; import {valueof, first, second, range, identity, lazyChannel, maybeLazyChannel, maybeTransform, maybeColor, maybeValue, mid, take, labelof} from "../mark.js"; import {offset} from "../style.js"; -import {groups} from "./group.js"; +import {maybeGroup} from "./group.js"; // Group on y, z, fill, or stroke, if any, then bin on x. export function binX({x, y, out = y == null ? "y" : "fill", inset, insetLeft, insetRight, ...options} = {}) { @@ -85,7 +85,7 @@ function bin1(x, key, {[key]: k, z, fill, stroke, weight, domain, thresholds, no if (cumulative < 0) B.reverse(); for (const facet of facets) { const binFacet = []; - for (const [, I] of groups(facet, G)) { + for (const [, I] of maybeGroup(facet, G)) { if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; const set = new Set(I); let f; @@ -163,7 +163,7 @@ function bin2(x, y, {weight, domain, thresholds, normalize, z, fill, stroke, ... let i = 0; for (const facet of facets) { const binFacet = []; - for (const [, I] of groups(facet, G)) { + for (const [, I] of maybeGroup(facet, G)) { if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; const set = new Set(I); for (const b of B) { diff --git a/src/transforms/group.js b/src/transforms/group.js index b29f28597d..0c813dbf31 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -99,10 +99,10 @@ function groupn( for (const facet of facets) { const groupFacet = []; // if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; // TODO - for (const [, I] of groups(facet, G)) { + for (const [, I] of maybeGroup(facet, G)) { // if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; // TODO - for (const [y, fy] of groups(I, Y)) { - for (const [x, f] of groups(fy, X)) { + for (const [y, fy] of maybeGroup(I, Y)) { + for (const [x, f] of maybeGroup(fy, X)) { // const l = W ? sum(f, i => W[i]) : f.length; // TODO groupFacet.push(i++); groupData.push(reduceData.reduce(f, data)); @@ -135,7 +135,7 @@ function groupn( // throw new Error("invalid normalize"); // } -export function groups(I, X) { +export function maybeGroup(I, X) { return X ? sort(grouper(I, i => X[i]), first) : [[, I]]; } From 720348726bbb0a5026b2a75b9eeb2ade7f5c4d59 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 11:09:03 -0700 Subject: [PATCH 16/48] reduce percent --- src/transforms/group.js | 19 ++++++++++++++++--- test/plots/word-length-moby-dick.js | 6 ++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 0c813dbf31..c4b1bc9285 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,6 +1,6 @@ import {group as grouper, sort, sum, deviation, min, max, mean, median, variance} from "d3"; import {firstof} from "../defined.js"; -import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take} from "../mark.js"; +import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof} from "../mark.js"; // Group on {z, fill, stroke}. export function groupZ(outputs, options) { @@ -36,9 +36,9 @@ function groupn( // Prepare the output channels: detect the corresponding inputs and reducers. outputs = Object.entries(outputs).map(([name, reduce]) => { const reducer = maybeReduce(reduce); - const value = reducer === reduceCount || reducer === reduceProportion ? identity : maybeInput(name, inputs); // TODO + const value = reducer.value == null ? identity : maybeInput(name, inputs); if (value == null) throw new Error(`missing channel: ${name}`); - const [output, setOutput] = lazyChannel(value); + const [output, setOutput] = lazyChannel(labelof(value, reducer.label)); let V, O; return { name, @@ -146,6 +146,7 @@ function maybeReduce(reduce) { case "first": return reduceFirst; case "last": return reduceLast; case "count": return reduceCount; + case "percent": return reducePercent; case "proportion": return reduceProportion; case "deviation": return reduceAccessor(deviation); case "min": return reduceAccessor(min); @@ -193,13 +194,25 @@ const reduceLast = { }; const reduceCount = { + value: null, + label: "Frequency", reduce(I) { return I.length; } }; const reduceProportion = { + value: null, + label: "Frequency", reduce(I, X) { return I.length / X.length; } }; + +const reducePercent = { + value: null, + label: "Frequency (%)", + reduce(I, X) { + return 100 * I.length / X.length; + } +}; diff --git a/test/plots/word-length-moby-dick.js b/test/plots/word-length-moby-dick.js index 09a3d04103..6b47fd5de6 100644 --- a/test/plots/word-length-moby-dick.js +++ b/test/plots/word-length-moby-dick.js @@ -17,12 +17,10 @@ export default async function() { labelAnchor: "right" }, y: { - grid: true, - transform: d => d * 100, - label: "↑ Frequency (%)" + grid: true }, marks: [ - Plot.barY(words, Plot.groupX({y: "proportion"}, {x: d => d.length})) + Plot.barY(words, Plot.groupX({y: "percent"}, {x: d => d.length})) ] }); } From 6cf3fc44e5c1c92cb8eea34efab2d5b5ba4e2446 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 11:12:34 -0700 Subject: [PATCH 17/48] fix value-less reducers --- src/transforms/group.js | 2 +- test/plots/penguin-species-group.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index c4b1bc9285..09105c8a40 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -36,7 +36,7 @@ function groupn( // Prepare the output channels: detect the corresponding inputs and reducers. outputs = Object.entries(outputs).map(([name, reduce]) => { const reducer = maybeReduce(reduce); - const value = reducer.value == null ? identity : maybeInput(name, inputs); + const value = reducer.value === null ? identity : maybeInput(name, inputs); if (value == null) throw new Error(`missing channel: ${name}`); const [output, setOutput] = lazyChannel(labelof(value, reducer.label)); let V, O; diff --git a/test/plots/penguin-species-group.js b/test/plots/penguin-species-group.js index 459129e512..a6fb816432 100644 --- a/test/plots/penguin-species-group.js +++ b/test/plots/penguin-species-group.js @@ -5,8 +5,8 @@ export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ marks: [ - Plot.barX(penguins, Plot.stackX(Plot.groupZ({x: "count"}, {fill: "species", normalize: true}))), - Plot.text(penguins, Plot.stackXMid(Plot.groupZ({x: "count"}, {z: "species", normalize: true, text: ([d]) => d.species}))), + Plot.barX(penguins, Plot.stackX(Plot.groupZ({x: "percent"}, {fill: "species"}))), + Plot.text(penguins, Plot.stackXMid(Plot.groupZ({x: "percent", text: "first"}, {z: "species", text: "species"}))), Plot.ruleX([0, 100]) ] }); From 4bcfb865b74c42ed5a7fa2b2c3b80a4765df9782 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 12:13:47 -0700 Subject: [PATCH 18/48] remove unused import --- src/transforms/bin.js | 2 +- src/transforms/group.js | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index d707c7ea08..97a15f5789 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -1,4 +1,4 @@ -import {bin as binner, cross, group, sum} from "d3"; +import {bin as binner, cross, sum} from "d3"; import {firstof} from "../defined.js"; import {valueof, first, second, range, identity, lazyChannel, maybeLazyChannel, maybeTransform, maybeColor, maybeValue, mid, take, labelof} from "../mark.js"; import {offset} from "../style.js"; diff --git a/src/transforms/group.js b/src/transforms/group.js index 09105c8a40..3832fe0976 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -30,7 +30,7 @@ function groupn( x, // optionally group on x y, // optionally group on y {data: reduceData = reduceIdentity, ...outputs} = {}, // output channel definitions - {domain, ...inputs} = {} // input channels and options + inputs = {} // input channels and options ) { // Prepare the output channels: detect the corresponding inputs and reducers. @@ -125,16 +125,6 @@ function groupn( }; } -// function maybeNormalize(normalize) { -// if (!normalize) return; -// if (normalize === true) return 100; -// if (typeof normalize === "number") return normalize; -// switch ((normalize + "").toLowerCase()) { -// case "facet": case "z": return 100; -// } -// throw new Error("invalid normalize"); -// } - export function maybeGroup(I, X) { return X ? sort(grouper(I, i => X[i]), first) : [[, I]]; } From 16ed1e0e8f379b568b77b4606a41ae23302eeeca Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 13:21:13 -0700 Subject: [PATCH 19/48] update tests --- test/output/athletesWeight.svg | 1184 +++++------ test/output/mobyDickLetterPosition.svg | 494 ++--- .../mobyDickLetterRelativeFrequency.svg | 167 ++ test/output/seattleTemperatureCell.svg | 1731 +++++++++++++---- test/plots/index.js | 1 + .../moby-dick-letter-relative-frequency.js | 16 + 6 files changed, 2389 insertions(+), 1204 deletions(-) create mode 100644 test/output/mobyDickLetterRelativeFrequency.svg create mode 100644 test/plots/moby-dick-letter-relative-frequency.js diff --git a/test/output/athletesWeight.svg b/test/output/athletesWeight.svg index 7a04a6d9b6..46c6f6c084 100644 --- a/test/output/athletesWeight.svg +++ b/test/output/athletesWeight.svg @@ -113,6 +113,80 @@ weight → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -172,6 +246,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -202,221 +413,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -443,145 +439,56 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -614,28 +521,6 @@ - - - - - - - - - - - - - - - - - - - - - - @@ -661,6 +546,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -680,197 +611,68 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -897,6 +699,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -921,30 +815,136 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/mobyDickLetterPosition.svg b/test/output/mobyDickLetterPosition.svg index adc5b83d5d..2b11905f22 100644 --- a/test/output/mobyDickLetterPosition.svg +++ b/test/output/mobyDickLetterPosition.svg @@ -124,252 +124,252 @@ Position within word - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/mobyDickLetterRelativeFrequency.svg b/test/output/mobyDickLetterRelativeFrequency.svg new file mode 100644 index 0000000000..8b4481c86f --- /dev/null +++ b/test/output/mobyDickLetterRelativeFrequency.svg @@ -0,0 +1,167 @@ + + + + + 0 + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + 5 + + + + 6 + + + + 7 + + + + 8 + + + + 9 + + + + 10 + + + + 11 + + + + 12 + ↑ Frequency (%) + + + + A + + + B + + + C + + + D + + + E + + + F + + + G + + + H + + + I + + + J + + + K + + + L + + + M + + + N + + + O + + + P + + + Q + + + R + + + S + + + T + + + U + + + V + + + W + + + X + + + Y + + + Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/seattleTemperatureCell.svg b/test/output/seattleTemperatureCell.svg index d15413161b..1a7724f216 100644 --- a/test/output/seattleTemperatureCell.svg +++ b/test/output/seattleTemperatureCell.svg @@ -133,371 +133,1372 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index 130cf515bc..f3283b3965 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -40,6 +40,7 @@ export {default as metroUnemploymentRidgeline} from "./metro-unemployment-ridgel export {default as mobyDickFaceted} from "./moby-dick-faceted.js"; export {default as mobyDickLetterFrequency} from "./moby-dick-letter-frequency.js"; export {default as mobyDickLetterPosition} from "./moby-dick-letter-position.js"; +export {default as mobyDickLetterRelativeFrequency} from "./moby-dick-letter-relative-frequency.js"; export {default as morleyBoxplot} from "./morley-boxplot.js"; export {default as moviesProfitByGenre} from "./movies-profit-by-genre.js"; export {default as musicRevenue} from "./music-revenue.js"; diff --git a/test/plots/moby-dick-letter-relative-frequency.js b/test/plots/moby-dick-letter-relative-frequency.js new file mode 100644 index 0000000000..6eabfc5bfa --- /dev/null +++ b/test/plots/moby-dick-letter-relative-frequency.js @@ -0,0 +1,16 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const mobydick = await d3.text("data/moby-dick-chapter-1.txt"); + const letters = [...mobydick].filter(c => /[a-z]/i.test(c)).map(c => c.toUpperCase()); + return Plot.plot({ + y: { + grid: true + }, + marks: [ + Plot.barY(letters, Plot.groupX({y: "percent"})), + Plot.ruleY([0]) + ] + }); +} From b66c361dd2e3939108dad64b1573b7ecadaf3d0a Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 13:32:16 -0700 Subject: [PATCH 20/48] more tests --- test/output/athletesHeightWeight.svg | 10974 +++++++++++++++++++++ test/output/athletesHeightWeightBin.svg | 931 ++ test/output/athletesNationality.svg | 112 + test/output/athletesSexWeight.svg | 127 + test/output/athletesSportWeight.svg | 950 ++ test/output/athletesWeight.svg | 990 +- test/plots/athletes-height-weight-bin.js | 20 + test/plots/athletes-height-weight.js | 13 + test/plots/athletes-nationality.js | 19 + test/plots/athletes-sex-weight.js | 15 + test/plots/athletes-sport-weight.js | 20 + test/plots/athletes-weight.js | 11 +- test/plots/index.js | 5 + 13 files changed, 13253 insertions(+), 934 deletions(-) create mode 100644 test/output/athletesHeightWeight.svg create mode 100644 test/output/athletesHeightWeightBin.svg create mode 100644 test/output/athletesNationality.svg create mode 100644 test/output/athletesSexWeight.svg create mode 100644 test/output/athletesSportWeight.svg create mode 100644 test/plots/athletes-height-weight-bin.js create mode 100644 test/plots/athletes-height-weight.js create mode 100644 test/plots/athletes-nationality.js create mode 100644 test/plots/athletes-sex-weight.js create mode 100644 test/plots/athletes-sport-weight.js diff --git a/test/output/athletesHeightWeight.svg b/test/output/athletesHeightWeight.svg new file mode 100644 index 0000000000..e555656714 --- /dev/null +++ b/test/output/athletesHeightWeight.svg @@ -0,0 +1,10974 @@ + + + + + 1.25 + + + + 1.30 + + + + 1.35 + + + + 1.40 + + + + 1.45 + + + + 1.50 + + + + 1.55 + + + + 1.60 + + + + 1.65 + + + + 1.70 + + + + 1.75 + + + + 1.80 + + + + 1.85 + + + + 1.90 + + + + 1.95 + + + + 2.00 + + + + 2.05 + + + + 2.10 + + + + 2.15 + + + + 2.20 + ↑ height + + + + + 40 + + + + 60 + + + + 80 + + + + 100 + + + + 120 + + + + 140 + + + + 160 + weight → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/athletesHeightWeightBin.svg b/test/output/athletesHeightWeightBin.svg new file mode 100644 index 0000000000..af6b696d38 --- /dev/null +++ b/test/output/athletesHeightWeightBin.svg @@ -0,0 +1,931 @@ + + + + + 1.2 + + + + 1.3 + + + + 1.4 + + + + 1.5 + + + + 1.6 + + + + 1.7 + + + + 1.8 + + + + 1.9 + + + + 2.0 + + + + 2.1 + + + + 2.2 + ↑ height + + + + + 40 + + + + 60 + + + + 80 + + + + 100 + + + + 120 + + + + 140 + + + + 160 + weight → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/athletesNationality.svg b/test/output/athletesNationality.svg new file mode 100644 index 0000000000..a7ad7ae35e --- /dev/null +++ b/test/output/athletesNationality.svg @@ -0,0 +1,112 @@ + + + + USA + + + BRA + + + GER + + + AUS + + + FRA + + + CHN + + + GBR + + + JPN + + + CAN + + + ESP + + + ITA + + + RUS + + + NED + + + POL + + + ARG + + + KOR + + + NZL + + + UKR + + + SWE + + + COL + + + + + + 0 + + + + 100 + + + + 200 + + + + 300 + + + + 400 + + + + 500 + Frequency → + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/athletesSexWeight.svg b/test/output/athletesSexWeight.svg new file mode 100644 index 0000000000..09b3d81c5a --- /dev/null +++ b/test/output/athletesSexWeight.svg @@ -0,0 +1,127 @@ + + + + + 0 + + + + 100 + + + + 200 + + + + 300 + + + + 400 + + + + 500 + + + + 600 + + + + 700 + + + + 800 + + + + 900 + + + + 1,000 + ↑ Frequency + + + + 40 + + + 60 + + + 80 + + + 100 + + + 120 + + + 140 + + + 160 + weight → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/athletesSportWeight.svg b/test/output/athletesSportWeight.svg new file mode 100644 index 0000000000..46c6f6c084 --- /dev/null +++ b/test/output/athletesSportWeight.svg @@ -0,0 +1,950 @@ + + + + aquatics + + + archery + + + athletics + + + badminton + + + basketball + + + canoe + + + cycling + + + equestrian + + + fencing + + + football + + + golf + + + gymnastics + + + handball + + + hockey + + + judo + + + modern pentathlon + + + rowing + + + rugby sevens + + + sailing + + + shooting + + + table tennis + + + taekwondo + + + tennis + + + triathlon + + + volleyball + + + weightlifting + + + wrestling + sport + + + + + 40 + + + + 60 + + + + 80 + + + + 100 + + + + 120 + + + + 140 + + + + 160 + weight → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/athletesWeight.svg b/test/output/athletesWeight.svg index 46c6f6c084..7d0e98057e 100644 --- a/test/output/athletesWeight.svg +++ b/test/output/athletesWeight.svg @@ -1,950 +1,92 @@ - - - - aquatics + + + + 0 - - archery + + 200 - - athletics + + 400 - - badminton + + 600 - - basketball + + 800 - - canoe + + 1,000 - - cycling + + 1,200 - - equestrian + + 1,400 - - fencing + + 1,600 - - football + + 1,800 - - golf + + 2,000 - - gymnastics + + 2,200 - - handball + + 2,400 - - hockey + + 2,600 - - judo - - - modern pentathlon - - - rowing - - - rugby sevens - - - sailing - - - shooting - - - table tennis - - - taekwondo - - - tennis - - - triathlon - - - volleyball - - - weightlifting - - - wrestling - sport + + 2,800 + ↑ Frequency - - - - 40 + + + 40 + + + 60 - - - 60 + + 80 - - - 80 + + 100 - - - 100 + + 120 - - - 120 + + 140 - - - 140 + + 160 - - - 160 + + 180 weight → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/athletes-height-weight-bin.js b/test/plots/athletes-height-weight-bin.js new file mode 100644 index 0000000000..b7b6c27f8f --- /dev/null +++ b/test/plots/athletes-height-weight-bin.js @@ -0,0 +1,20 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const athletes = await d3.csv("data/athletes.csv", d3.autoType); + return Plot.plot({ + round: true, + grid: true, + height: 640, + y: { + ticks: 10 + }, + color: { + scheme: "YlGnBu" + }, + marks: [ + Plot.rect(athletes, Plot.bin({x: "weight", y: "height", thresholds: 50})) + ] + }); +} diff --git a/test/plots/athletes-height-weight.js b/test/plots/athletes-height-weight.js new file mode 100644 index 0000000000..f9387dfe41 --- /dev/null +++ b/test/plots/athletes-height-weight.js @@ -0,0 +1,13 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const athletes = await d3.csv("data/athletes.csv", d3.autoType); + return Plot.plot({ + grid: true, + height: 640, + marks: [ + Plot.dot(athletes, {x: "weight", y: "height"}) + ] + }); +} diff --git a/test/plots/athletes-nationality.js b/test/plots/athletes-nationality.js new file mode 100644 index 0000000000..d4d0fd6602 --- /dev/null +++ b/test/plots/athletes-nationality.js @@ -0,0 +1,19 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const athletes = await d3.csv("data/athletes.csv", d3.autoType); + const top = new Set(d3.groupSort(athletes, g => -g.length, d => d.nationality).slice(0, 20)); + return Plot.plot({ + x: { + grid: true + }, + y: { + domain: top, + label: null + }, + marks: [ + Plot.barX(athletes, Plot.groupY({x: "count"}, {filter: d => top.has(d.nationality), y: "nationality"})) // TODO remove filter + ] + }); +} diff --git a/test/plots/athletes-sex-weight.js b/test/plots/athletes-sex-weight.js new file mode 100644 index 0000000000..d49e532033 --- /dev/null +++ b/test/plots/athletes-sex-weight.js @@ -0,0 +1,15 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const athletes = await d3.csv("data/athletes.csv", d3.autoType); + return Plot.plot({ + y: { + grid: true + }, + marks: [ + Plot.rectY(athletes, Plot.binX({x: "weight", fill: "sex", mixBlendMode: "multiply", thresholds: 30})), + Plot.ruleY([0]) + ] + }); +} diff --git a/test/plots/athletes-sport-weight.js b/test/plots/athletes-sport-weight.js new file mode 100644 index 0000000000..176c9b81c5 --- /dev/null +++ b/test/plots/athletes-sport-weight.js @@ -0,0 +1,20 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const athletes = await d3.csv("data/athletes.csv", d3.autoType); + return Plot.plot({ + marginLeft: 100, + height: 640, + x: { + grid: true + }, + color: { + scheme: "YlGnBu", + zero: true + }, + marks: [ + Plot.barX(athletes, Plot.binX({x: "weight", y: "sport", thresholds: 60, normalize: "z", out: "fill"})) + ] + }); +} diff --git a/test/plots/athletes-weight.js b/test/plots/athletes-weight.js index 176c9b81c5..e4eec014d0 100644 --- a/test/plots/athletes-weight.js +++ b/test/plots/athletes-weight.js @@ -4,17 +4,8 @@ import * as d3 from "d3"; export default async function() { const athletes = await d3.csv("data/athletes.csv", d3.autoType); return Plot.plot({ - marginLeft: 100, - height: 640, - x: { - grid: true - }, - color: { - scheme: "YlGnBu", - zero: true - }, marks: [ - Plot.barX(athletes, Plot.binX({x: "weight", y: "sport", thresholds: 60, normalize: "z", out: "fill"})) + Plot.rectY(athletes, Plot.binX({x: "weight"})) ] }); } diff --git a/test/plots/index.js b/test/plots/index.js index f3283b3965..8b2897e60d 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -3,6 +3,11 @@ export {default as aaplChangeVolume} from "./aapl-change-volume.js"; export {default as aaplClose} from "./aapl-close.js"; export {default as aaplVolume} from "./aapl-volume.js"; export {default as anscombeQuartet} from "./anscombe-quartet.js"; +export {default as athletesHeightWeight} from "./athletes-height-weight.js"; +export {default as athletesHeightWeightBin} from "./athletes-height-weight-bin.js"; +export {default as athletesNationality} from "./athletes-nationality.js"; +export {default as athletesSexWeight} from "./athletes-sex-weight.js"; +export {default as athletesSportWeight} from "./athletes-sport-weight.js"; export {default as athletesWeight} from "./athletes-weight.js"; export {default as ballotStatusRace} from "./ballot-status-race.js"; export {default as beckerBarley} from "./becker-barley.js"; From d7dc200e313bb2c78f3313c41241fb8a3fedc7d3 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 13:57:21 -0700 Subject: [PATCH 21/48] smarter facet axis position --- src/axes.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/axes.js b/src/axes.js index 21266f300d..0a959a8154 100644 --- a/src/axes.js +++ b/src/axes.js @@ -8,15 +8,15 @@ export function Axes( let {axis: yAxis = true} = y; let {axis: fxAxis = true} = fx; let {axis: fyAxis = true} = fy; - if (xAxis === true) xAxis = "bottom"; - if (yAxis === true) yAxis = "left"; - if (fxAxis === true) fxAxis = xAxis === "bottom" ? "top" : "bottom"; - if (fyAxis === true) fyAxis = yAxis === "left" ? "right" : "left"; + if (!xScale) xAxis = null; else if (xAxis === true) xAxis = "bottom"; + if (!yScale) yAxis = null; else if (yAxis === true) yAxis = "left"; + if (!fxScale) fxAxis = null; else if (fxAxis === true) fxAxis = xAxis === "bottom" ? "top" : "bottom"; + if (!fyScale) fyAxis = null; else if (fyAxis === true) fyAxis = yAxis === "left" ? "right" : "left"; return { - ...xScale && xAxis && {x: new AxisX({grid, ...x, axis: xAxis})}, - ...yScale && yAxis && {y: new AxisY({grid, ...y, axis: yAxis})}, - ...fxScale && fxAxis && {fx: new AxisX({name: "fx", grid: facetGrid, ...fx, axis: fxAxis})}, - ...fyScale && fyAxis && {fy: new AxisY({name: "fy", grid: facetGrid, ...fy, axis: fyAxis})} + ...xAxis && {x: new AxisX({grid, ...x, axis: xAxis})}, + ...yAxis && {y: new AxisY({grid, ...y, axis: yAxis})}, + ...fxAxis && {fx: new AxisX({name: "fx", grid: facetGrid, ...fx, axis: fxAxis})}, + ...fyAxis && {fy: new AxisY({name: "fy", grid: facetGrid, ...fy, axis: fyAxis})} }; } From c12160f91affab4556ef8b685c3401a0e7d2490c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 13:57:58 -0700 Subject: [PATCH 22/48] percent-z, percent-facet --- src/transforms/group.js | 36 +++++++++++++++++++++++++++--------- test/plots/index.js | 1 + 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 3832fe0976..6b67930968 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -47,8 +47,8 @@ function groupn( V = valueof(data, value); O = setOutput([]); }, - reduce(I) { - O.push(reducer.reduce(I, V)); + reduce(group, context, facet) { + O.push(reducer.reduce(group, V, context, facet)); } }; }); @@ -101,19 +101,19 @@ function groupn( // if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; // TODO for (const [, I] of maybeGroup(facet, G)) { // if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; // TODO - for (const [y, fy] of maybeGroup(I, Y)) { - for (const [x, f] of maybeGroup(fy, X)) { + for (const [y, gg] of maybeGroup(I, Y)) { + for (const [x, g] of maybeGroup(gg, X)) { // const l = W ? sum(f, i => W[i]) : f.length; // TODO groupFacet.push(i++); - groupData.push(reduceData.reduce(f, data)); + groupData.push(reduceData.reduce(g, data)); // BL.push(m ? l * m / n : l); if (X) BX.push(x); if (Y) BY.push(y); - if (Z) BZ.push(Z[f[0]]); - if (F) BF.push(F[f[0]]); - if (S) BS.push(S[f[0]]); + if (Z) BZ.push(Z[g[0]]); + if (F) BF.push(F[g[0]]); + if (S) BS.push(S[g[0]]); for (const output of outputs) { - output.reduce(f); + output.reduce(g, I, facet); } } } @@ -137,6 +137,8 @@ function maybeReduce(reduce) { case "last": return reduceLast; case "count": return reduceCount; case "percent": return reducePercent; + case "percent-facet": return reducePercentFacet; // TODO cleaner + case "percent-z": return reducePercentZ; // TODO cleaner case "proportion": return reduceProportion; case "deviation": return reduceAccessor(deviation); case "min": return reduceAccessor(min); @@ -206,3 +208,19 @@ const reducePercent = { return 100 * I.length / X.length; } }; + +const reducePercentFacet = { + value: null, + label: "Frequency (%)", + reduce(I, X, context, facet) { + return 100 * I.length / facet.length; + } +}; + +const reducePercentZ = { + value: null, + label: "Frequency (%)", + reduce(I, X, context) { + return 100 * I.length / context.length; + } +}; diff --git a/test/plots/index.js b/test/plots/index.js index 8b2897e60d..bdd1ab8652 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -59,6 +59,7 @@ export {default as penguinMassSpecies} from "./penguin-mass-species.js"; export {default as penguinSexMassCulmenSpecies} from "./penguin-sex-mass-culmen-species.js"; export {default as penguinSpeciesGroup} from "./penguin-species-group.js"; export {default as penguinSpeciesIsland} from "./penguin-species-island.js"; +export {default as penguinSpeciesIslandRelative} from "./penguin-species-island-relative.js"; export {default as policeDeaths} from "./police-deaths.js"; export {default as policeDeathsBar} from "./police-deaths-bar.js"; export {default as randomWalk} from "./random-walk.js"; From cfd94fa415598aa3647d76d28146ce1df6e68e85 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 14:41:22 -0700 Subject: [PATCH 23/48] generalize normalize --- src/transforms/group.js | 75 +++++++++++++---------------------------- 1 file changed, 23 insertions(+), 52 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 6b67930968..84b57efd66 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,6 +1,6 @@ import {group as grouper, sort, sum, deviation, min, max, mean, median, variance} from "d3"; import {firstof} from "../defined.js"; -import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof} from "../mark.js"; +import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range} from "../mark.js"; // Group on {z, fill, stroke}. export function groupZ(outputs, options) { @@ -30,8 +30,9 @@ function groupn( x, // optionally group on x y, // optionally group on y {data: reduceData = reduceIdentity, ...outputs} = {}, // output channel definitions - inputs = {} // input channels and options + {normalize, ...inputs} = {} // input channels and options ) { + normalize = maybeNormalize(normalize); // Prepare the output channels: detect the corresponding inputs and reducers. outputs = Object.entries(outputs).map(([name, reduce]) => { @@ -39,7 +40,7 @@ function groupn( const value = reducer.value === null ? identity : maybeInput(name, inputs); if (value == null) throw new Error(`missing channel: ${name}`); const [output, setOutput] = lazyChannel(labelof(value, reducer.label)); - let V, O; + let V, O, b = 1; return { name, output, @@ -47,8 +48,11 @@ function groupn( V = valueof(data, value); O = setOutput([]); }, - reduce(group, context, facet) { - O.push(reducer.reduce(group, V, context, facet)); + basis(group) { + b = reducer.reduce(group, V); + }, + reduce(group) { + O.push(reducer.reduce(group, V) / b); } }; }); @@ -91,30 +95,24 @@ function groupn( const BZ = Z && setBZ([]); const BF = F && setBF([]); const BS = S && setBS([]); - // let n = W ? sum(W) : data.length; // TODO let i = 0; - for (const output of outputs) { - output.initialize(data); - } + for (const o of outputs) o.initialize(data); + if (normalize === true) for (const o of outputs) o.basis(range(data)); for (const facet of facets) { const groupFacet = []; - // if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; // TODO + if (normalize === "facet") for (const o of outputs) o.basis(facet); for (const [, I] of maybeGroup(facet, G)) { - // if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; // TODO + if (normalize === "z") for (const o of outputs) o.basis(I); for (const [y, gg] of maybeGroup(I, Y)) { for (const [x, g] of maybeGroup(gg, X)) { - // const l = W ? sum(f, i => W[i]) : f.length; // TODO groupFacet.push(i++); groupData.push(reduceData.reduce(g, data)); - // BL.push(m ? l * m / n : l); if (X) BX.push(x); if (Y) BY.push(y); if (Z) BZ.push(Z[g[0]]); if (F) BF.push(F[g[0]]); if (S) BS.push(S[g[0]]); - for (const output of outputs) { - output.reduce(g, I, facet); - } + for (const o of outputs) o.reduce(g); } } } @@ -129,6 +127,15 @@ export function maybeGroup(I, X) { return X ? sort(grouper(I, i => X[i]), first) : [[, I]]; } +function maybeNormalize(normalize) { + if (!normalize) return; + if (normalize === true) return; + switch ((normalize + "").toLowerCase()) { + case "z": case "facet": return normalize; + } + throw new Error("invalid normalize"); +} + function maybeReduce(reduce) { if (reduce && typeof reduce.reduce === "function") return reduce; if (typeof reduce === "function") return reduceFunction(reduce); @@ -136,10 +143,6 @@ function maybeReduce(reduce) { case "first": return reduceFirst; case "last": return reduceLast; case "count": return reduceCount; - case "percent": return reducePercent; - case "percent-facet": return reducePercentFacet; // TODO cleaner - case "percent-z": return reducePercentZ; // TODO cleaner - case "proportion": return reduceProportion; case "deviation": return reduceAccessor(deviation); case "min": return reduceAccessor(min); case "max": return reduceAccessor(max); @@ -192,35 +195,3 @@ const reduceCount = { return I.length; } }; - -const reduceProportion = { - value: null, - label: "Frequency", - reduce(I, X) { - return I.length / X.length; - } -}; - -const reducePercent = { - value: null, - label: "Frequency (%)", - reduce(I, X) { - return 100 * I.length / X.length; - } -}; - -const reducePercentFacet = { - value: null, - label: "Frequency (%)", - reduce(I, X, context, facet) { - return 100 * I.length / facet.length; - } -}; - -const reducePercentZ = { - value: null, - label: "Frequency (%)", - reduce(I, X, context) { - return 100 * I.length / context.length; - } -}; From e66e14139579594d3f64a972c08e0838f8b8d99c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 14:42:50 -0700 Subject: [PATCH 24/48] add test output --- test/plots/penguin-species-island-relative.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/plots/penguin-species-island-relative.js diff --git a/test/plots/penguin-species-island-relative.js b/test/plots/penguin-species-island-relative.js new file mode 100644 index 0000000000..c11b712614 --- /dev/null +++ b/test/plots/penguin-species-island-relative.js @@ -0,0 +1,19 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + fx: { + tickSize: 6 + }, + facet: { + data: penguins, + x: "species" + }, + marks: [ + Plot.barY(penguins, Plot.stackY(Plot.groupZ({y: "count"}, {normalize: "facet", fill: "island"}))), + Plot.ruleY([0]) + ] + }); +} From 6353dd2dee3b59e5c830c9da9187bd249fa0cfbe Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Mar 2021 16:27:05 -0700 Subject: [PATCH 25/48] normalize --- src/transforms/group.js | 7 +- .../mobyDickLetterRelativeFrequency.svg | 32 ++++---- test/output/penguinSpeciesGroup.svg | 30 ++++---- test/output/penguinSpeciesIslandRelative.svg | 76 +++++++++++++++++++ test/output/wordLengthMobyDick.svg | 32 ++++---- .../moby-dick-letter-relative-frequency.js | 2 +- test/plots/penguin-species-group.js | 6 +- test/plots/word-length-moby-dick.js | 2 +- 8 files changed, 132 insertions(+), 55 deletions(-) create mode 100644 test/output/penguinSpeciesIslandRelative.svg diff --git a/src/transforms/group.js b/src/transforms/group.js index 84b57efd66..569e4e794f 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -40,7 +40,7 @@ function groupn( const value = reducer.value === null ? identity : maybeInput(name, inputs); if (value == null) throw new Error(`missing channel: ${name}`); const [output, setOutput] = lazyChannel(labelof(value, reducer.label)); - let V, O, b = 1; + let V, O, b; return { name, output, @@ -52,7 +52,8 @@ function groupn( b = reducer.reduce(group, V); }, reduce(group) { - O.push(reducer.reduce(group, V) / b); + const v = reducer.reduce(group, V); + O.push(b === undefined ? v : v / b); } }; }); @@ -129,7 +130,7 @@ export function maybeGroup(I, X) { function maybeNormalize(normalize) { if (!normalize) return; - if (normalize === true) return; + if (normalize === true) return true; switch ((normalize + "").toLowerCase()) { case "z": case "facet": return normalize; } diff --git a/test/output/mobyDickLetterRelativeFrequency.svg b/test/output/mobyDickLetterRelativeFrequency.svg index 8b4481c86f..7e4a35b6c9 100644 --- a/test/output/mobyDickLetterRelativeFrequency.svg +++ b/test/output/mobyDickLetterRelativeFrequency.svg @@ -2,56 +2,56 @@ - 0 + 0.00 - 1 + 0.01 - 2 + 0.02 - 3 + 0.03 - 4 + 0.04 - 5 + 0.05 - 6 + 0.06 - 7 + 0.07 - 8 + 0.08 - 9 + 0.09 - 10 + 0.10 - 11 + 0.11 - 12 - ↑ Frequency (%) + 0.12 + ↑ Frequency @@ -136,7 +136,7 @@ - + @@ -147,7 +147,7 @@ - + diff --git a/test/output/penguinSpeciesGroup.svg b/test/output/penguinSpeciesGroup.svg index 3f1492bbb6..d62ef62a20 100644 --- a/test/output/penguinSpeciesGroup.svg +++ b/test/output/penguinSpeciesGroup.svg @@ -1,45 +1,45 @@ - 0 + 0.0 - 10 + 0.1 - 20 + 0.2 - 30 + 0.3 - 40 + 0.4 - 50 + 0.5 - 60 + 0.6 - 70 + 0.7 - 80 + 0.8 - 90 + 0.9 - 100 - Frequency (%) → + 1.0 + Frequency → - - + + - AdelieChinstrapGentoo + NaNNaNNaN diff --git a/test/output/penguinSpeciesIslandRelative.svg b/test/output/penguinSpeciesIslandRelative.svg new file mode 100644 index 0000000000..64b8d4d54a --- /dev/null +++ b/test/output/penguinSpeciesIslandRelative.svg @@ -0,0 +1,76 @@ + + + + 0.0 + + + 0.1 + + + 0.2 + + + 0.3 + + + 0.4 + + + 0.5 + + + 0.6 + + + 0.7 + + + 0.8 + + + 0.9 + + + 1.0 + ↑ Frequency + + + + Adelie + + + Chinstrap + + + Gentoo + species + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/wordLengthMobyDick.svg b/test/output/wordLengthMobyDick.svg index 55db63244d..db719b1f0f 100644 --- a/test/output/wordLengthMobyDick.svg +++ b/test/output/wordLengthMobyDick.svg @@ -2,48 +2,48 @@ - 0 + 0.00 - 2 + 0.02 - 4 + 0.04 - 6 + 0.06 - 8 + 0.08 - 10 + 0.10 - 12 + 0.12 - 14 + 0.14 - 16 + 0.16 - 18 + 0.18 - 20 - ↑ Frequency (%) + 0.20 + ↑ Frequency @@ -91,11 +91,11 @@ - + - - - + + + diff --git a/test/plots/moby-dick-letter-relative-frequency.js b/test/plots/moby-dick-letter-relative-frequency.js index 6eabfc5bfa..2e25aa8aca 100644 --- a/test/plots/moby-dick-letter-relative-frequency.js +++ b/test/plots/moby-dick-letter-relative-frequency.js @@ -9,7 +9,7 @@ export default async function() { grid: true }, marks: [ - Plot.barY(letters, Plot.groupX({y: "percent"})), + Plot.barY(letters, Plot.groupX({y: "count"}, {normalize: true})), Plot.ruleY([0]) ] }); diff --git a/test/plots/penguin-species-group.js b/test/plots/penguin-species-group.js index a6fb816432..a731836876 100644 --- a/test/plots/penguin-species-group.js +++ b/test/plots/penguin-species-group.js @@ -5,9 +5,9 @@ export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ marks: [ - Plot.barX(penguins, Plot.stackX(Plot.groupZ({x: "percent"}, {fill: "species"}))), - Plot.text(penguins, Plot.stackXMid(Plot.groupZ({x: "percent", text: "first"}, {z: "species", text: "species"}))), - Plot.ruleX([0, 100]) + Plot.barX(penguins, Plot.stackX(Plot.groupZ({x: "count"}, {normalize: true, fill: "species"}))), + Plot.text(penguins, Plot.stackXMid(Plot.groupZ({x: "count", text: "first"}, {normalize: true, z: "species", text: "species"}))), // TODO only normalize x, not text + Plot.ruleX([0, 1]) ] }); } diff --git a/test/plots/word-length-moby-dick.js b/test/plots/word-length-moby-dick.js index 6b47fd5de6..deafb75b9b 100644 --- a/test/plots/word-length-moby-dick.js +++ b/test/plots/word-length-moby-dick.js @@ -20,7 +20,7 @@ export default async function() { grid: true }, marks: [ - Plot.barY(words, Plot.groupX({y: "percent"}, {x: d => d.length})) + Plot.barY(words, Plot.groupX({y: "count"}, {normalize: true, x: d => d.length})) ] }); } From 5a09d1117830baa67b0ddd9322822ee6e0cf447c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 09:26:27 -0700 Subject: [PATCH 26/48] proportional reducers --- src/transforms/group.js | 53 ++++++++++--------- test/output/penguinSpeciesGroup.svg | 2 +- .../moby-dick-letter-relative-frequency.js | 2 +- test/plots/penguin-species-group.js | 4 +- test/plots/penguin-species-island-relative.js | 2 +- test/plots/word-length-moby-dick.js | 2 +- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 569e4e794f..61f17da6fc 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -30,30 +30,33 @@ function groupn( x, // optionally group on x y, // optionally group on y {data: reduceData = reduceIdentity, ...outputs} = {}, // output channel definitions - {normalize, ...inputs} = {} // input channels and options + inputs = {} // input channels and options ) { - normalize = maybeNormalize(normalize); // Prepare the output channels: detect the corresponding inputs and reducers. outputs = Object.entries(outputs).map(([name, reduce]) => { - const reducer = maybeReduce(reduce); - const value = reducer.value === null ? identity : maybeInput(name, inputs); - if (value == null) throw new Error(`missing channel: ${name}`); + const value = maybeInput(name, inputs); + const reducer = maybeReduce(reduce, value); const [output, setOutput] = lazyChannel(labelof(value, reducer.label)); - let V, O, b; + let V, O, basis; return { name, output, initialize(data) { V = valueof(data, value); O = setOutput([]); + if (reducer.scope === SCOPE_DATA) { + basis = reducer.reduce(range(data), V); + } }, - basis(group) { - b = reducer.reduce(group, V); + scope(scope, I) { + if (reducer.scope === scope) { + basis = reducer.reduce(I, V); + } }, - reduce(group) { - const v = reducer.reduce(group, V); - O.push(b === undefined ? v : v / b); + reduce(I) { + const v = reducer.reduce(I, V); + O.push(reducer.scope ? v / basis : v); } }; }); @@ -98,12 +101,11 @@ function groupn( const BS = S && setBS([]); let i = 0; for (const o of outputs) o.initialize(data); - if (normalize === true) for (const o of outputs) o.basis(range(data)); for (const facet of facets) { const groupFacet = []; - if (normalize === "facet") for (const o of outputs) o.basis(facet); + for (const o of outputs) o.scope(SCOPE_FACET, facet); for (const [, I] of maybeGroup(facet, G)) { - if (normalize === "z") for (const o of outputs) o.basis(I); + for (const o of outputs) o.scope(SCOPE_Z, facet); for (const [y, gg] of maybeGroup(I, Y)) { for (const [x, g] of maybeGroup(gg, X)) { groupFacet.push(i++); @@ -128,28 +130,22 @@ export function maybeGroup(I, X) { return X ? sort(grouper(I, i => X[i]), first) : [[, I]]; } -function maybeNormalize(normalize) { - if (!normalize) return; - if (normalize === true) return true; - switch ((normalize + "").toLowerCase()) { - case "z": case "facet": return normalize; - } - throw new Error("invalid normalize"); -} - -function maybeReduce(reduce) { +function maybeReduce(reduce, value) { if (reduce && typeof reduce.reduce === "function") return reduce; if (typeof reduce === "function") return reduceFunction(reduce); switch ((reduce + "").toLowerCase()) { case "first": return reduceFirst; case "last": return reduceLast; case "count": return reduceCount; + case "sum": return value == null ? reduceCount : reduceSum; + case "proportion": return {...value == null ? reduceCount : reduceSum, scope: SCOPE_DATA}; + case "proportion-facet": return {...value == null ? reduceCount : reduceSum, scope: SCOPE_FACET}; + case "proportion-z": return {...value == null ? reduceCount : reduceSum, scope: SCOPE_Z}; case "deviation": return reduceAccessor(deviation); case "min": return reduceAccessor(min); case "max": return reduceAccessor(max); case "mean": return reduceAccessor(mean); case "median": return reduceAccessor(median); - case "sum": return reduceAccessor(sum); case "variance": return reduceAccessor(variance); } throw new Error("invalid reduce"); @@ -190,9 +186,14 @@ const reduceLast = { }; const reduceCount = { - value: null, label: "Frequency", reduce(I) { return I.length; } }; + +const reduceSum = reduceAccessor(sum); + +const SCOPE_DATA = Symbol("data"); +const SCOPE_FACET = Symbol("facet"); +const SCOPE_Z = Symbol("z"); diff --git a/test/output/penguinSpeciesGroup.svg b/test/output/penguinSpeciesGroup.svg index d62ef62a20..304c1ad761 100644 --- a/test/output/penguinSpeciesGroup.svg +++ b/test/output/penguinSpeciesGroup.svg @@ -39,7 +39,7 @@ - NaNNaNNaN + AdelieChinstrapGentoo diff --git a/test/plots/moby-dick-letter-relative-frequency.js b/test/plots/moby-dick-letter-relative-frequency.js index 2e25aa8aca..2a87c8df19 100644 --- a/test/plots/moby-dick-letter-relative-frequency.js +++ b/test/plots/moby-dick-letter-relative-frequency.js @@ -9,7 +9,7 @@ export default async function() { grid: true }, marks: [ - Plot.barY(letters, Plot.groupX({y: "count"}, {normalize: true})), + Plot.barY(letters, Plot.groupX({y: "proportion"})), Plot.ruleY([0]) ] }); diff --git a/test/plots/penguin-species-group.js b/test/plots/penguin-species-group.js index a731836876..846e92b9c1 100644 --- a/test/plots/penguin-species-group.js +++ b/test/plots/penguin-species-group.js @@ -5,8 +5,8 @@ export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ marks: [ - Plot.barX(penguins, Plot.stackX(Plot.groupZ({x: "count"}, {normalize: true, fill: "species"}))), - Plot.text(penguins, Plot.stackXMid(Plot.groupZ({x: "count", text: "first"}, {normalize: true, z: "species", text: "species"}))), // TODO only normalize x, not text + Plot.barX(penguins, Plot.stackX(Plot.groupZ({x: "proportion"}, {fill: "species"}))), + Plot.text(penguins, Plot.stackXMid(Plot.groupZ({x: "proportion", text: "first"}, {z: "species", text: "species"}))), Plot.ruleX([0, 1]) ] }); diff --git a/test/plots/penguin-species-island-relative.js b/test/plots/penguin-species-island-relative.js index c11b712614..26e0e06c7f 100644 --- a/test/plots/penguin-species-island-relative.js +++ b/test/plots/penguin-species-island-relative.js @@ -12,7 +12,7 @@ export default async function() { x: "species" }, marks: [ - Plot.barY(penguins, Plot.stackY(Plot.groupZ({y: "count"}, {normalize: "facet", fill: "island"}))), + Plot.barY(penguins, Plot.stackY(Plot.groupZ({y: "proportion-facet"}, {fill: "island"}))), Plot.ruleY([0]) ] }); diff --git a/test/plots/word-length-moby-dick.js b/test/plots/word-length-moby-dick.js index deafb75b9b..9850ca6d48 100644 --- a/test/plots/word-length-moby-dick.js +++ b/test/plots/word-length-moby-dick.js @@ -20,7 +20,7 @@ export default async function() { grid: true }, marks: [ - Plot.barY(words, Plot.groupX({y: "count"}, {normalize: true, x: d => d.length})) + Plot.barY(words, Plot.groupX({y: "proportion"}, {x: "length"})) ] }); } From 2b7bb684ea6706115ce8b6f892a8974c30e2c4cb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 09:37:29 -0700 Subject: [PATCH 27/48] allow external scoped reducers --- src/transforms/group.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 61f17da6fc..2b18f36d4f 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,4 +1,4 @@ -import {group as grouper, sort, sum, deviation, min, max, mean, median, variance} from "d3"; +import {group as grouper, sort, sum, deviation, min, max, mean, median, variance, curveBasis} from "d3"; import {firstof} from "../defined.js"; import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range} from "../mark.js"; @@ -38,25 +38,24 @@ function groupn( const value = maybeInput(name, inputs); const reducer = maybeReduce(reduce, value); const [output, setOutput] = lazyChannel(labelof(value, reducer.label)); - let V, O, basis; + let V, O, context; return { name, output, initialize(data) { V = valueof(data, value); O = setOutput([]); - if (reducer.scope === SCOPE_DATA) { - basis = reducer.reduce(range(data), V); + if (reducer.scope === "data") { + context = reducer.reduce(range(data), V); } }, scope(scope, I) { if (reducer.scope === scope) { - basis = reducer.reduce(I, V); + context = reducer.reduce(I, V); } }, reduce(I) { - const v = reducer.reduce(I, V); - O.push(reducer.scope ? v / basis : v); + O.push(reducer.reduce(I, V, context)); } }; }); @@ -103,9 +102,9 @@ function groupn( for (const o of outputs) o.initialize(data); for (const facet of facets) { const groupFacet = []; - for (const o of outputs) o.scope(SCOPE_FACET, facet); + for (const o of outputs) o.scope("facet", facet); for (const [, I] of maybeGroup(facet, G)) { - for (const o of outputs) o.scope(SCOPE_Z, facet); + for (const o of outputs) o.scope("z", facet); for (const [y, gg] of maybeGroup(I, Y)) { for (const [x, g] of maybeGroup(gg, X)) { groupFacet.push(i++); @@ -138,9 +137,9 @@ function maybeReduce(reduce, value) { case "last": return reduceLast; case "count": return reduceCount; case "sum": return value == null ? reduceCount : reduceSum; - case "proportion": return {...value == null ? reduceCount : reduceSum, scope: SCOPE_DATA}; - case "proportion-facet": return {...value == null ? reduceCount : reduceSum, scope: SCOPE_FACET}; - case "proportion-z": return {...value == null ? reduceCount : reduceSum, scope: SCOPE_Z}; + case "proportion": return reduceProportion(value, "data"); + case "proportion-facet": return reduceProportion(value, "facet"); + case "proportion-z": return reduceProportion(value, "z"); case "deviation": return reduceAccessor(deviation); case "min": return reduceAccessor(min); case "max": return reduceAccessor(max); @@ -194,6 +193,8 @@ const reduceCount = { const reduceSum = reduceAccessor(sum); -const SCOPE_DATA = Symbol("data"); -const SCOPE_FACET = Symbol("facet"); -const SCOPE_Z = Symbol("z"); +function reduceProportion(value, scope) { + return value == null + ? {scope, label: "Frequency", reduce: (I, V, basis = 1) => I.length / basis} + : {scope, reduce: (I, V, basis = 1) => sum(I, i => V[i]) / basis}; +} From cb03034ba9ea228b04ccf39225703895aea80b2b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 09:39:31 -0700 Subject: [PATCH 28/48] bad vscode --- src/transforms/group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 2b18f36d4f..5261098db0 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,4 +1,4 @@ -import {group as grouper, sort, sum, deviation, min, max, mean, median, variance, curveBasis} from "d3"; +import {group as grouper, sort, sum, deviation, min, max, mean, median, variance} from "d3"; import {firstof} from "../defined.js"; import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range} from "../mark.js"; From a717b7cfa8c55bb865b09268b9a993dfbf41f330 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 09:57:52 -0700 Subject: [PATCH 29/48] require group channels --- src/transforms/group.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/transforms/group.js b/src/transforms/group.js index 5261098db0..13cfb1621e 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -10,12 +10,14 @@ export function groupZ(outputs, options) { // Group on {z, fill, stroke}, then on x (optionally). export function groupX(outputs, options = {}) { const {x = identity} = options; + if (x == null) throw new Error("missing channel: x"); return groupn(x, null, outputs, options); } // Group on {z, fill, stroke}, then on y (optionally). export function groupY(outputs, options = {}) { const {y = identity} = options; + if (y == null) throw new Error("missing channel: y"); return groupn(null, y, outputs, options); } @@ -23,6 +25,8 @@ export function groupY(outputs, options = {}) { export function group(outputs, options = {}) { let {x, y} = options; ([x, y] = maybeTuple(x, y)); + if (x == null) throw new Error("missing channel: x"); + if (y == null) throw new Error("missing channel: y"); return groupn(x, y, outputs, options); } From cbfbdf8650b2e30b9be0aaabbbacea89548beb75 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 11:56:39 -0700 Subject: [PATCH 30/48] bin outputs --- src/index.js | 2 +- src/mark.js | 1 + src/transforms/bin.js | 323 ++++++++---------- src/transforms/group.js | 116 +++---- test/plots/aapl-volume.js | 2 +- test/plots/athletes-height-weight-bin.js | 2 +- test/plots/athletes-sex-weight.js | 2 +- test/plots/athletes-sport-weight.js | 8 +- test/plots/athletes-weight.js | 2 +- test/plots/diamonds-carat-price-dots.js | 2 +- test/plots/diamonds-carat-price.js | 2 +- test/plots/penguin-mass-sex-species.js | 2 +- test/plots/penguin-mass-sex.js | 2 +- test/plots/penguin-mass-species.js | 2 +- test/plots/penguin-mass.js | 2 +- test/plots/penguin-sex-mass-culmen-species.js | 2 +- test/plots/uniform-random-difference.js | 2 +- 17 files changed, 218 insertions(+), 256 deletions(-) diff --git a/src/index.js b/src/index.js index 08c993f41e..7efca6372f 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ export {Rect, rect, rectX, rectY} from "./marks/rect.js"; export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js"; export {Text, text, textX, textY} from "./marks/text.js"; export {TickX, TickY, tickX, tickY} from "./marks/tick.js"; -export {bin, binX, binY, binXMid, binYMid, binR} from "./transforms/bin.js"; +export {bin, binX, binY, binXMid, binYMid, binMid} from "./transforms/bin.js"; export {group, groupX, groupY, groupZ} from "./transforms/group.js"; export {normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; diff --git a/src/mark.js b/src/mark.js index 8791c6ccae..03907d42b9 100644 --- a/src/mark.js +++ b/src/mark.js @@ -222,6 +222,7 @@ export function mid(x1, x2) { transform(data) { const X1 = x1.transform(data); const X2 = x2.transform(data); + console.log(X1, X2); return Float64Array.from(X1, (_, i) => (X1[i] + X2[i]) / 2); }, label: x1.label diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 97a15f5789..a7e133620f 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -1,222 +1,179 @@ -import {bin as binner, cross, sum} from "d3"; +import {bin as binner} from "d3"; import {firstof} from "../defined.js"; -import {valueof, first, second, range, identity, lazyChannel, maybeLazyChannel, maybeTransform, maybeColor, maybeValue, mid, take, labelof} from "../mark.js"; +import {valueof, range, identity, maybeLazyChannel, maybeTransform, maybeTuple, maybeColor, maybeValue, mid} from "../mark.js"; import {offset} from "../style.js"; -import {maybeGroup} from "./group.js"; +import {maybeGroup, maybeGroupOutputs, reduceIdentity} from "./group.js"; -// Group on y, z, fill, or stroke, if any, then bin on x. -export function binX({x, y, out = y == null ? "y" : "fill", inset, insetLeft, insetRight, ...options} = {}) { +// TODO remove optional group dimension, and rely on facet instead + +// Group on {z, fill, stroke}, then optionally on y, then bin x. +export function binX(outputs, {domain, thresholds, inset, insetLeft, insetRight, ...options} = {}) { + let {x, y} = options; + x = maybeBinValue(x, {domain, thresholds}, identity); ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); - const [transform, x1, x2, l] = bin1(x, "y", {y, ...options}); - return {x1, x2, ...transform, inset, insetLeft, insetRight, [out]: l}; + return binn(x, null, null, y, outputs, {inset, insetLeft, insetRight, ...options}); } -// Group on y, z, fill, or stroke, if any, then bin on x. -export function binXMid({x, out = "r", ...options} = {}) { - const [transform, x1, x2, l] = bin1(x, "y", options); - return {x: mid(x1, x2), ...transform, [out]: l}; +// Group on {z, fill, stroke}, then optionally on y, then bin x. +export function binXMid(outputs, {domain, thresholds, ...options} = {}) { + let {x, y} = options; + x = maybeBinValue(x, {domain, thresholds}, identity); + const {x1, x2, ...transform} = binn(x, null, null, y, outputs, options); + return {...transform, x: mid(x1, x2)}; } -// Group on x, z, fill, or stroke, if any, then bin on y. -export function binY({y, x, out = x == null ? "x" : "fill", inset, insetTop, insetBottom, ...options} = {}) { +// Group on {z, fill, stroke}, then optionally on x, then bin y. +export function binY(outputs, {domain, thresholds, inset, insetTop, insetBottom, ...options} = {}) { + let {x, y} = options; + y = maybeBinValue(y, {domain, thresholds}, identity); ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); - const [transform, y1, y2, l] = bin1(y, "x", {x, ...options}); - return {y1, y2, ...transform, inset, insetTop, insetBottom, [out]: l}; + return binn(null, y, x, null, outputs, {inset, insetLeft, insetRight, ...options}); } -// Group on y, z, fill, or stroke, if any, then bin on x. -export function binYMid({y, out = "r", ...options} = {}) { - const [transform, y1, y2, l] = bin1(y, "x", options); - return {y: mid(y1, y2), ...transform, [out]: l}; +// Group on {z, fill, stroke}, then optionally on x, then bin y. +export function binYMid(outputs, {domain, thresholds, ...options} = {}) { + let {x, y} = options; + y = maybeBinValue(y, {domain, thresholds}, identity); + const {y1, y2, ...transform} = binn(null, x, y, null, outputs, options); + return {...transform, y: mid(y1, y2)}; } -// Group on z, fill, or stroke, if any, then bin on x and y. -export function binR({x, y, ...options} = {}) { - const [transform, x1, x2, y1, y2, r] = bin2(x, y, options); - return {x: mid(x1, x2), y: mid(y1, y2), r, ...transform}; +// Group on {z, fill, stroke}, then bin on x and y. +export function binMid(outputs, options) { + const {x1, x2, y1, y2, ...transform} = bin(outputs, options); + return {...transform, x: mid(x1, x2), y: mid(y1, y2)}; // TODO don’t set insets } -// Group on z, fill, or stroke, if any, then bin on x and y. -export function bin({x, y, out = "fill", inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) { +// Group on {z, fill, stroke}, then bin on x and y. +export function bin(outputs, {domain, thresholds, inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) { + let {x, y} = options; + x = maybeBinValue(x, {domain, thresholds}); + y = maybeBinValue(y, {domain, thresholds}); + ([x.value, y.value] = maybeTuple(x.value, y.value)); ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); - const [transform, x1, x2, y1, y2, l] = bin2(x, y, options); - return {x1, x2, y1, y2, ...transform, inset, insetTop, insetRight, insetBottom, insetLeft, [out]: l}; + return binn(x, y, null, null, outputs, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options}); } -function bin1(x, key, {[key]: k, z, fill, stroke, weight, domain, thresholds, normalize, cumulative, ...options} = {}) { - const m = normalize === true || normalize === "z" ? 100 : +normalize; - const bin = binof(identity, {value: x, domain, thresholds}); - const [X1, setX1] = lazyChannel(x); - const [X2, setX2] = lazyChannel(x); - const [L, setL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); +// TODO cumulative (per dimension) +function binn( + bx, // optionally bin on x (exclusive with gx) + by, // optionally bin on y (exclusive with gy) + gx, // optionally group on x (exclusive with bx and gy) + gy, // optionally group on y (exclusive with by and gx) + {data: reduceData = reduceIdentity, ...outputs} = {}, // output channel definitions + inputs = {} // input channels and options +) { + bx = maybeBin(bx); + by = maybeBin(by); + outputs = maybeGroupOutputs(outputs, inputs); + + // Produce x1, x2, y1, and y2 output channels as appropriate (when binning). + const [BX1, setBX1] = maybeLazyChannel(bx); + const [BX2, setBX2] = maybeLazyChannel(bx); + const [BY1, setBY1] = maybeLazyChannel(by); + const [BY2, setBY2] = maybeLazyChannel(by); + + // Produce x or y output channels as appropriate (when grouping). + const [k, gk] = gx != null ? [gx, "x"] : gy != null ? [gy, "y"] : []; + const [GK, setGK] = maybeLazyChannel(k); + + // Greedily materialize the z, fill, and stroke channels (if channels and not + // constants) so that we can reference them for subdividing groups without + // computing them more than once. + const {z, fill, stroke, ...options} = inputs; + const [GZ, setGZ] = maybeLazyChannel(z); const [vfill] = maybeColor(fill); const [vstroke] = maybeColor(stroke); - const [BK, setBK] = maybeLazyChannel(k); - const [BZ, setBZ] = maybeLazyChannel(z); - const [BF = fill, setBF] = maybeLazyChannel(vfill); - const [BS = stroke, setBS] = maybeLazyChannel(vstroke); - return [ - { - ...key && {[key]: BK}, - z: BZ, - fill: BF, - stroke: BS, - ...options, - transform: maybeTransform(options, (data, facets) => { - const B = bin(data); - const K = valueof(data, k); - const Z = valueof(data, z); - const F = valueof(data, vfill); - const S = valueof(data, vstroke); - const W = valueof(data, weight); - const binFacets = []; - const binData = []; - const X1 = setX1([]); - const X2 = setX2([]); - const L = setL([]); - const G = firstof(K, Z, F, S); - const BK = K && setBK([]); - const BZ = Z && setBZ([]); - const BF = F && setBF([]); - const BS = S && setBS([]); - let n = W ? sum(W) : data.length; - let i = 0; - if (cumulative < 0) B.reverse(); - for (const facet of facets) { - const binFacet = []; - for (const [, I] of maybeGroup(facet, G)) { - if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; - const set = new Set(I); - let f; - for (const b of B) { - const s = b.filter(i => set.has(i)); - f = cumulative && f !== undefined ? f.concat(s) : s; - const l = W ? sum(f, i => W[i]) : f.length; - if (l > 0) { - binFacet.push(i++); - binData.push(take(data, f)); - X1.push(b.x0); - X2.push(b.x1); - L.push(m ? l * m / n : l); - if (K) BK.push(K[f[0]]); - if (Z) BZ.push(Z[f[0]]); - if (F) BF.push(F[f[0]]); - if (S) BS.push(S[f[0]]); + const [GF = fill, setGF] = maybeLazyChannel(vfill); + const [GS = stroke, setGS] = maybeLazyChannel(vstroke); + + return { + z: GZ, + fill: GF, + stroke: GS, + ...options, + ...GK && {[gk]: GK}, + ...BX1 && {x1: BX1, x2: BX2}, + ...BY1 && {y1: BY1, y2: BY2}, + ...Object.fromEntries(outputs.map(({name, output}) => [name, output])), + transform: maybeTransform(options, (data, facets) => { + const K = valueof(data, k); + const Z = valueof(data, z); + const F = valueof(data, vfill); + const S = valueof(data, vstroke); + const G = firstof(Z, F, S); + const groupFacets = []; + const groupData = []; + const GK = K && setGK([]); + const GZ = Z && setGZ([]); + const GF = F && setGF([]); + const GS = S && setGS([]); + const BX = bx ? bx(data).filter(nonempty).map(binset) : [[,, I => I]]; + const BY = by ? by(data).filter(nonempty).map(binset) : [[,, I => I]]; + const BX1 = bx && setBX1([]); + const BX2 = bx && setBX2([]); + const BY1 = by && setBY1([]); + const BY2 = by && setBY2([]); + let i = 0; + for (const o of outputs) o.initialize(data); + for (const facet of facets) { + const groupFacet = []; + for (const o of outputs) o.scope("facet", facet); + for (const [, I] of maybeGroup(facet, G)) { + for (const o of outputs) o.scope("z", I); + for (const [k, g] of maybeGroup(I, G)) { + for (const [x1, x2, fx] of BX) { + const bb = fx(g); + if (bb.length === 0) continue; + for (const [y1, y2, fy] of BY) { + const b = fy(bb); + if (b.length === 0) continue; + groupFacet.push(i++); + groupData.push(reduceData.reduce(b, data)); + if (K) GK.push(k); + if (Z) GZ.push(Z[b[0]]); + if (F) GF.push(F[b[0]]); + if (S) GS.push(S[b[0]]); + if (BX1) BX1.push(x1), BX2.push(x2); + if (BY1) BY1.push(y1), BY2.push(y2); + for (const o of outputs) o.reduce(b); } } } - binFacets.push(binFacet); } - return {data: binData, facets: binFacets}; - }) - }, - X1, - X2, - L - ]; + groupFacets.push(groupFacet); + } + return {data: groupData, facets: groupFacets}; + }) + }; } -// Here x and y may each either be a standalone value (e.g., a string -// representing a field name, a function, an array), or the value and some -// additional per-dimension binning options as an objects of the form {value, -// domain?, thresholds?}. -function bin2(x, y, {weight, domain, thresholds, normalize, z, fill, stroke, ...options} = {}) { - const m = normalize === true || normalize === "z" ? 100 : +normalize; - const binX = binof(first, {domain, thresholds, ...maybeValue(x)}); - const binY = binof(second, {domain, thresholds, ...maybeValue(y)}); - const bin = data => cross(binX(data).filter(nonempty), binY(data).filter(nonempty).map(binset2), (x, y) => y(x)); - const [X1, setX1] = lazyChannel(x); - const [X2, setX2] = lazyChannel(x); - const [Y1, setY1] = lazyChannel(y); - const [Y2, setY2] = lazyChannel(y); - const [L, setL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); - const [vfill] = maybeColor(fill); - const [vstroke] = maybeColor(stroke); - const [BZ, setBZ] = maybeLazyChannel(z); - const [BF = fill, setBF] = maybeLazyChannel(vfill); - const [BS = stroke, setBS] = maybeLazyChannel(vstroke); - return [ - { - z: BZ, - fill: BF, - stroke: BS, - ...options, - transform: maybeTransform(options, (data, facets) => { - const B = bin(data); - const Z = valueof(data, z); - const F = valueof(data, vfill); - const S = valueof(data, vstroke); - const W = valueof(data, weight); - const binFacets = []; - const binData = []; - const X1 = setX1([]); - const X2 = setX2([]); - const Y1 = setY1([]); - const Y2 = setY2([]); - const L = setL([]); - const G = firstof(Z, F, S); - const BZ = Z && setBZ([]); - const BF = F && setBF([]); - const BS = S && setBS([]); - let n = W ? sum(W) : data.length; - let i = 0; - for (const facet of facets) { - const binFacet = []; - for (const [, I] of maybeGroup(facet, G)) { - if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; - const set = new Set(I); - for (const b of B) { - const f = b.filter(i => set.has(i)); - const l = W ? sum(f, i => W[i]) : f.length; - if (l > 0) { - binFacet.push(i++); - binData.push(take(data, f)); - X1.push(b.x0); - X2.push(b.x1); - Y1.push(b.y0); - Y2.push(b.y1); - L.push(m ? l * m / n : l); - if (Z) BZ.push(Z[f[0]]); - if (F) BF.push(F[f[0]]); - if (S) BS.push(S[f[0]]); - } - } - } - binFacets.push(binFacet); - } - return {data: binData, facets: binFacets}; - }) - }, - X1, - X2, - Y1, - Y2, - L - ]; +function maybeBinValue(value, {domain, thresholds} = {}, defaultValue) { + value = {...maybeValue(value)}; + // console.log(value, thresholds); + if (value.domain === undefined) value.domain = domain; + if (value.thresholds === undefined) value.thresholds = thresholds; + if (value.value === undefined) value.value = defaultValue; + return value; } -function binof(defaultValue, {value = defaultValue, domain, thresholds}) { +function maybeBin(options) { + if (options == null) return; + const {value, domain, thresholds} = options; return data => { - const values = valueof(data, value); - const bin = binner().value(i => values[i]); + const V = valueof(data, value); + const bin = binner().value(i => V[i]); if (domain !== undefined) bin.domain(domain); if (thresholds !== undefined) bin.thresholds(thresholds); return bin(range(data)); }; } -function binset2(biny) { - const y = new Set(biny); - const {x0: y0, x1: y1} = biny; - return binx => { - const subbin = binx.filter(i => y.has(i)); - subbin.x0 = binx.x0; - subbin.x1 = binx.x1; - subbin.y0 = y0; - subbin.y1 = y1; - return subbin; - }; +function binset(bin) { + const S = []; + for (const i of bin) S[i] = 1; + return [bin.x0, bin.x1, I => I.filter(i => S[i])]; } function nonempty({length}) { diff --git a/src/transforms/group.js b/src/transforms/group.js index 13cfb1621e..e0c164a423 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -7,21 +7,21 @@ export function groupZ(outputs, options) { return groupn(null, null, outputs, options); } -// Group on {z, fill, stroke}, then on x (optionally). +// Group on {z, fill, stroke}, then on x. export function groupX(outputs, options = {}) { const {x = identity} = options; if (x == null) throw new Error("missing channel: x"); return groupn(x, null, outputs, options); } -// Group on {z, fill, stroke}, then on y (optionally). +// Group on {z, fill, stroke}, then on y. export function groupY(outputs, options = {}) { const {y = identity} = options; if (y == null) throw new Error("missing channel: y"); return groupn(null, y, outputs, options); } -// Group on {z, fill, stroke}, then on x and y (optionally). +// Group on {z, fill, stroke}, then on x and y. export function group(outputs, options = {}) { let {x, y} = options; ([x, y] = maybeTuple(x, y)); @@ -36,57 +36,29 @@ function groupn( {data: reduceData = reduceIdentity, ...outputs} = {}, // output channel definitions inputs = {} // input channels and options ) { + outputs = maybeGroupOutputs(outputs, inputs); - // Prepare the output channels: detect the corresponding inputs and reducers. - outputs = Object.entries(outputs).map(([name, reduce]) => { - const value = maybeInput(name, inputs); - const reducer = maybeReduce(reduce, value); - const [output, setOutput] = lazyChannel(labelof(value, reducer.label)); - let V, O, context; - return { - name, - output, - initialize(data) { - V = valueof(data, value); - O = setOutput([]); - if (reducer.scope === "data") { - context = reducer.reduce(range(data), V); - } - }, - scope(scope, I) { - if (reducer.scope === scope) { - context = reducer.reduce(I, V); - } - }, - reduce(I) { - O.push(reducer.reduce(I, V, context)); - } - }; - }); - - // The x and y channels are used for grouping. Note that the passed-through - // options may also include x and y channels which are ignored, so we only - // want to generate these as output channels if they were used for grouping. - const [BX, setBX] = maybeLazyChannel(x); - const [BY, setBY] = maybeLazyChannel(y); + // Produce x and y output channels as appropriate. + const [GX, setGX] = maybeLazyChannel(x); + const [GY, setGY] = maybeLazyChannel(y); - // The z, fill, and stroke channels (if channels and not constants) are - // greedily materialized by the transform so that we can reference them for - // subdividing groups without having to compute them more than once. + // Greedily materialize the z, fill, and stroke channels (if channels and not + // constants) so that we can reference them for subdividing groups without + // computing them more than once. const {z, fill, stroke, ...options} = inputs; - const [BZ, setBZ] = maybeLazyChannel(z); + const [GZ, setGZ] = maybeLazyChannel(z); const [vfill] = maybeColor(fill); const [vstroke] = maybeColor(stroke); - const [BF = fill, setBF] = maybeLazyChannel(vfill); - const [BS = stroke, setBS] = maybeLazyChannel(vstroke); + const [GF = fill, setGF] = maybeLazyChannel(vfill); + const [GS = stroke, setGS] = maybeLazyChannel(vstroke); return { - z: BZ, - fill: BF, - stroke: BS, + z: GZ, + fill: GF, + stroke: GS, ...options, - ...BX && {x: BX}, - ...BY && {y: BY}, + ...GX && {x: GX}, + ...GY && {y: GY}, ...Object.fromEntries(outputs.map(({name, output}) => [name, output])), transform: maybeTransform(options, (data, facets) => { const X = valueof(data, x); @@ -97,27 +69,27 @@ function groupn( const G = firstof(Z, F, S); const groupFacets = []; const groupData = []; - const BX = X && setBX([]); - const BY = Y && setBY([]); - const BZ = Z && setBZ([]); - const BF = F && setBF([]); - const BS = S && setBS([]); + const GX = X && setGX([]); + const GY = Y && setGY([]); + const GZ = Z && setGZ([]); + const GF = F && setGF([]); + const GS = S && setGS([]); let i = 0; for (const o of outputs) o.initialize(data); for (const facet of facets) { const groupFacet = []; for (const o of outputs) o.scope("facet", facet); for (const [, I] of maybeGroup(facet, G)) { - for (const o of outputs) o.scope("z", facet); + for (const o of outputs) o.scope("z", I); for (const [y, gg] of maybeGroup(I, Y)) { for (const [x, g] of maybeGroup(gg, X)) { groupFacet.push(i++); groupData.push(reduceData.reduce(g, data)); - if (X) BX.push(x); - if (Y) BY.push(y); - if (Z) BZ.push(Z[g[0]]); - if (F) BF.push(F[g[0]]); - if (S) BS.push(S[g[0]]); + if (X) GX.push(x); + if (Y) GY.push(y); + if (Z) GZ.push(Z[g[0]]); + if (F) GF.push(F[g[0]]); + if (S) GS.push(S[g[0]]); for (const o of outputs) o.reduce(g); } } @@ -129,6 +101,34 @@ function groupn( }; } +export function maybeGroupOutputs(outputs, inputs) { + return Object.entries(outputs).map(([name, reduce]) => { + const value = maybeInput(name, inputs); + const reducer = maybeReduce(reduce, value); + const [output, setOutput] = lazyChannel(labelof(value, reducer.label)); + let V, O, context; + return { + name, + output, + initialize(data) { + V = valueof(data, value); + O = setOutput([]); + if (reducer.scope === "data") { + context = reducer.reduce(range(data), V); + } + }, + scope(scope, I) { + if (reducer.scope === scope) { + context = reducer.reduce(I, V); + } + }, + reduce(I) { + O.push(reducer.reduce(I, V, context)); + } + }; + }); +} + export function maybeGroup(I, X) { return X ? sort(grouper(I, i => X[i]), first) : [[, I]]; } @@ -170,7 +170,7 @@ function reduceAccessor(f) { }; } -const reduceIdentity = { +export const reduceIdentity = { reduce(I, X) { return take(X, I); } diff --git a/test/plots/aapl-volume.js b/test/plots/aapl-volume.js index e3c48ef90f..5311d91da1 100644 --- a/test/plots/aapl-volume.js +++ b/test/plots/aapl-volume.js @@ -12,7 +12,7 @@ export default async function() { grid: true }, marks: [ - Plot.rectY(data, Plot.binX({x: d => Math.log10(d.Volume), normalize: true})), + Plot.rectY(data, Plot.binX({y: "proportion"}, {x: d => Math.log10(d.Volume)})), Plot.ruleY([0]) ] }); diff --git a/test/plots/athletes-height-weight-bin.js b/test/plots/athletes-height-weight-bin.js index b7b6c27f8f..ef8e90ca1a 100644 --- a/test/plots/athletes-height-weight-bin.js +++ b/test/plots/athletes-height-weight-bin.js @@ -14,7 +14,7 @@ export default async function() { scheme: "YlGnBu" }, marks: [ - Plot.rect(athletes, Plot.bin({x: "weight", y: "height", thresholds: 50})) + Plot.rect(athletes, Plot.bin({fill: "count"}, {x: "weight", y: "height", thresholds: 50})) ] }); } diff --git a/test/plots/athletes-sex-weight.js b/test/plots/athletes-sex-weight.js index d49e532033..64a0db316b 100644 --- a/test/plots/athletes-sex-weight.js +++ b/test/plots/athletes-sex-weight.js @@ -8,7 +8,7 @@ export default async function() { grid: true }, marks: [ - Plot.rectY(athletes, Plot.binX({x: "weight", fill: "sex", mixBlendMode: "multiply", thresholds: 30})), + Plot.rectY(athletes, Plot.binX({y: "count"}, {x: "weight", fill: "sex", mixBlendMode: "multiply", thresholds: 30})), Plot.ruleY([0]) ] }); diff --git a/test/plots/athletes-sport-weight.js b/test/plots/athletes-sport-weight.js index 176c9b81c5..00da2f9f20 100644 --- a/test/plots/athletes-sport-weight.js +++ b/test/plots/athletes-sport-weight.js @@ -4,7 +4,6 @@ import * as d3 from "d3"; export default async function() { const athletes = await d3.csv("data/athletes.csv", d3.autoType); return Plot.plot({ - marginLeft: 100, height: 640, x: { grid: true @@ -13,8 +12,13 @@ export default async function() { scheme: "YlGnBu", zero: true }, + facet: { + data: athletes, + marginLeft: 100, + y: "sport" + }, marks: [ - Plot.barX(athletes, Plot.binX({x: "weight", y: "sport", thresholds: 60, normalize: "z", out: "fill"})) + Plot.barX(athletes, Plot.binX({fill: "proportion-facet"}, {x: "weight", thresholds: 60})) ] }); } diff --git a/test/plots/athletes-weight.js b/test/plots/athletes-weight.js index e4eec014d0..4af9cc255d 100644 --- a/test/plots/athletes-weight.js +++ b/test/plots/athletes-weight.js @@ -5,7 +5,7 @@ export default async function() { const athletes = await d3.csv("data/athletes.csv", d3.autoType); return Plot.plot({ marks: [ - Plot.rectY(athletes, Plot.binX({x: "weight"})) + Plot.rectY(athletes, Plot.binX({y: "count"}, {x: "weight"})) ] }); } diff --git a/test/plots/diamonds-carat-price-dots.js b/test/plots/diamonds-carat-price-dots.js index 4571e52718..b7430ce3c5 100644 --- a/test/plots/diamonds-carat-price-dots.js +++ b/test/plots/diamonds-carat-price-dots.js @@ -17,7 +17,7 @@ export default async function() { range: [0, 3] }, marks: [ - Plot.dot(data, Plot.binR({x: "carat", y: "price", thresholds: 100})) + Plot.dot(data, Plot.binMid({r: "count"}, {x: "carat", y: "price", thresholds: 100})) ] }); } diff --git a/test/plots/diamonds-carat-price.js b/test/plots/diamonds-carat-price.js index 6abba626a0..3a86705ab5 100644 --- a/test/plots/diamonds-carat-price.js +++ b/test/plots/diamonds-carat-price.js @@ -10,7 +10,7 @@ export default async function() { type: "symlog" }, marks: [ - Plot.rect(data, Plot.bin({x: "carat", y: "price", thresholds: 100})) + Plot.rect(data, Plot.bin({fill: "count"}, {x: "carat", y: "price", thresholds: 100})) ] }); } diff --git a/test/plots/penguin-mass-sex-species.js b/test/plots/penguin-mass-sex-species.js index 8627b3c789..3e03390d4f 100644 --- a/test/plots/penguin-mass-sex-species.js +++ b/test/plots/penguin-mass-sex-species.js @@ -15,7 +15,7 @@ export default async function() { marginRight: 70 }, marks: [ - Plot.rectY(data, Plot.binX({x: "body_mass_g"})), + Plot.rectY(data, Plot.binX({y: "count"}, {x: "body_mass_g"})), Plot.ruleY([0]) ] }); diff --git a/test/plots/penguin-mass-sex.js b/test/plots/penguin-mass-sex.js index ac225aa47b..01d8289990 100644 --- a/test/plots/penguin-mass-sex.js +++ b/test/plots/penguin-mass-sex.js @@ -14,7 +14,7 @@ export default async function() { marginRight: 70 }, marks: [ - Plot.rectY(data, Plot.binX({x: "body_mass_g"})), + Plot.rectY(data, Plot.binX({y: "count"}, {x: "body_mass_g"})), Plot.ruleY([0]) ] }); diff --git a/test/plots/penguin-mass-species.js b/test/plots/penguin-mass-species.js index 8d822f8103..83fc13c6c4 100644 --- a/test/plots/penguin-mass-species.js +++ b/test/plots/penguin-mass-species.js @@ -12,7 +12,7 @@ export default async function() { grid: true }, marks: [ - Plot.rectY(data, Plot.stackY(Plot.binX({x: "body_mass_g", fill: "species"}))), + Plot.rectY(data, Plot.stackY(Plot.binX({y: "count"}, {x: "body_mass_g", fill: "species"}))), Plot.ruleY([0]) ] }); diff --git a/test/plots/penguin-mass.js b/test/plots/penguin-mass.js index 831d9749a7..68fdc36ce5 100644 --- a/test/plots/penguin-mass.js +++ b/test/plots/penguin-mass.js @@ -12,7 +12,7 @@ export default async function() { grid: true }, marks: [ - Plot.rectY(data, Plot.binX({x: "body_mass_g"})), + Plot.rectY(data, Plot.binX({y: "count"}, {x: "body_mass_g"})), Plot.ruleY([0]) ] }); diff --git a/test/plots/penguin-sex-mass-culmen-species.js b/test/plots/penguin-sex-mass-culmen-species.js index a68b984ef1..be87ebd188 100644 --- a/test/plots/penguin-sex-mass-culmen-species.js +++ b/test/plots/penguin-sex-mass-culmen-species.js @@ -20,7 +20,7 @@ export default async function() { }, marks: [ Plot.frame(), - Plot.dot(data, Plot.binR({ + Plot.dot(data, Plot.binMid({r: "count"}, { x: "body_mass_g", y: "culmen_length_mm", stroke: "species", diff --git a/test/plots/uniform-random-difference.js b/test/plots/uniform-random-difference.js index fb719734d9..ba60340fc1 100644 --- a/test/plots/uniform-random-difference.js +++ b/test/plots/uniform-random-difference.js @@ -13,7 +13,7 @@ export default async function() { grid: true }, marks: [ - Plot.rectY({length: 10000}, Plot.binX({x: () => random() - random(), normalize: true})), + Plot.rectY({length: 10000}, Plot.binX({y: "proportion"}, {x: () => random() - random()})), Plot.ruleY([0]) ] }); From ed986999ddf67862575fadd039bfb8ad895669bb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 12:02:14 -0700 Subject: [PATCH 31/48] fix inset --- src/transforms/bin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index a7e133620f..9d9ebc5415 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -27,7 +27,7 @@ export function binY(outputs, {domain, thresholds, inset, insetTop, insetBottom, let {x, y} = options; y = maybeBinValue(y, {domain, thresholds}, identity); ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); - return binn(null, y, x, null, outputs, {inset, insetLeft, insetRight, ...options}); + return binn(null, y, x, null, outputs, {inset, insetTop, insetBottom, ...options}); } // Group on {z, fill, stroke}, then optionally on x, then bin y. From 5c97c25b04c0b18e5c62ee2c2f5b143734f59327 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 16:10:26 -0700 Subject: [PATCH 32/48] cumulative bins --- src/mark.js | 1 - src/transforms/bin.js | 75 +- test/output/aaplVolume.svg | 22 +- test/output/athletesHeightWeightBin.svg | 4 +- test/output/athletesSexWeight.svg | 2 +- test/output/athletesSportWeight.svg | 1918 ++++++++++--------- test/output/athletesWeight.svg | 2 +- test/output/athletesWeightCumulative.svg | 80 + test/output/diamondsCaratPrice.svg | 4 +- test/output/penguinMassSpecies.svg | 68 +- test/output/penguinSexMassCulmenSpecies.svg | 4 +- test/output/uniformRandomDifference.svg | 34 +- test/plots/athletes-weight-cumulative.js | 11 + test/plots/index.js | 1 + 14 files changed, 1219 insertions(+), 1007 deletions(-) create mode 100644 test/output/athletesWeightCumulative.svg create mode 100644 test/plots/athletes-weight-cumulative.js diff --git a/src/mark.js b/src/mark.js index 03907d42b9..8791c6ccae 100644 --- a/src/mark.js +++ b/src/mark.js @@ -222,7 +222,6 @@ export function mid(x1, x2) { transform(data) { const X1 = x1.transform(data); const X2 = x2.transform(data); - console.log(X1, X2); return Float64Array.from(X1, (_, i) => (X1[i] + X2[i]) / 2); }, label: x1.label diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 9d9ebc5415..bfec09292b 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -4,36 +4,34 @@ import {valueof, range, identity, maybeLazyChannel, maybeTransform, maybeTuple, import {offset} from "../style.js"; import {maybeGroup, maybeGroupOutputs, reduceIdentity} from "./group.js"; -// TODO remove optional group dimension, and rely on facet instead - // Group on {z, fill, stroke}, then optionally on y, then bin x. -export function binX(outputs, {domain, thresholds, inset, insetLeft, insetRight, ...options} = {}) { +export function binX(outputs, {inset, insetLeft, insetRight, ...options} = {}) { let {x, y} = options; - x = maybeBinValue(x, {domain, thresholds}, identity); + x = maybeBinValue(x, options, identity); ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); return binn(x, null, null, y, outputs, {inset, insetLeft, insetRight, ...options}); } // Group on {z, fill, stroke}, then optionally on y, then bin x. -export function binXMid(outputs, {domain, thresholds, ...options} = {}) { +export function binXMid(outputs, options = {}) { let {x, y} = options; - x = maybeBinValue(x, {domain, thresholds}, identity); + x = maybeBinValue(x, options, identity); const {x1, x2, ...transform} = binn(x, null, null, y, outputs, options); return {...transform, x: mid(x1, x2)}; } // Group on {z, fill, stroke}, then optionally on x, then bin y. -export function binY(outputs, {domain, thresholds, inset, insetTop, insetBottom, ...options} = {}) { +export function binY(outputs, {inset, insetTop, insetBottom, ...options} = {}) { let {x, y} = options; - y = maybeBinValue(y, {domain, thresholds}, identity); + y = maybeBinValue(y, options, identity); ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); return binn(null, y, x, null, outputs, {inset, insetTop, insetBottom, ...options}); } // Group on {z, fill, stroke}, then optionally on x, then bin y. -export function binYMid(outputs, {domain, thresholds, ...options} = {}) { +export function binYMid(outputs, options = {}) { let {x, y} = options; - y = maybeBinValue(y, {domain, thresholds}, identity); + y = maybeBinValue(y, options, identity); const {y1, y2, ...transform} = binn(null, x, y, null, outputs, options); return {...transform, y: mid(y1, y2)}; } @@ -45,17 +43,16 @@ export function binMid(outputs, options) { } // Group on {z, fill, stroke}, then bin on x and y. -export function bin(outputs, {domain, thresholds, inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) { +export function bin(outputs, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) { let {x, y} = options; - x = maybeBinValue(x, {domain, thresholds}); - y = maybeBinValue(y, {domain, thresholds}); + x = maybeBinValue(x, options); + y = maybeBinValue(y, options); ([x.value, y.value] = maybeTuple(x.value, y.value)); ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); return binn(x, y, null, null, outputs, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options}); } -// TODO cumulative (per dimension) function binn( bx, // optionally bin on x (exclusive with gx) by, // optionally bin on y (exclusive with gy) @@ -109,8 +106,8 @@ function binn( const GZ = Z && setGZ([]); const GF = F && setGF([]); const GS = S && setGS([]); - const BX = bx ? bx(data).filter(nonempty).map(binset) : [[,, I => I]]; - const BY = by ? by(data).filter(nonempty).map(binset) : [[,, I => I]]; + const BX = bx ? bx(data) : [[,, I => I]]; + const BY = by ? by(data) : [[,, I => I]]; const BX1 = bx && setBX1([]); const BX2 = bx && setBX2([]); const BY1 = by && setBY1([]); @@ -149,10 +146,10 @@ function binn( }; } -function maybeBinValue(value, {domain, thresholds} = {}, defaultValue) { +function maybeBinValue(value, {cumulative, domain, thresholds} = {}, defaultValue) { value = {...maybeValue(value)}; - // console.log(value, thresholds); if (value.domain === undefined) value.domain = domain; + if (value.cumulative === undefined) value.cumulative = cumulative; if (value.thresholds === undefined) value.thresholds = thresholds; if (value.value === undefined) value.value = defaultValue; return value; @@ -160,31 +157,53 @@ function maybeBinValue(value, {domain, thresholds} = {}, defaultValue) { function maybeBin(options) { if (options == null) return; - const {value, domain, thresholds} = options; + const {value, cumulative, domain, thresholds} = options; return data => { const V = valueof(data, value); const bin = binner().value(i => V[i]); if (domain !== undefined) bin.domain(domain); if (thresholds !== undefined) bin.thresholds(thresholds); - return bin(range(data)); + let bins = bin.value(i => V[i])(range(data)).map(binset); + if (cumulative) bins = bins.map(bincumset); + return bins.filter(nonempty2).map(binfilter); }; } function binset(bin) { - const S = []; - for (const i of bin) S[i] = 1; - return [bin.x0, bin.x1, I => I.filter(i => S[i])]; + return [bin, new Set(bin)]; } -function nonempty({length}) { - return length > 0; +function bincumset([bin], j, bins) { + return [ + bin, + { + get size() { + for (let k = 0; k <= j; ++k) { + if (bins[k][1].size) { + return 1; // a non-empty value + } + } + return 0; + }, + has(i) { + for (let k = 0; k <= j; ++k) { + if (bins[k][1].has(i)) { + return true; + } + } + return false; + } + } + ]; } -function length1({length}) { - return length; +function binfilter([{x0, x1}, set]) { + return [x0, x1, I => I.filter(set.has, set)]; // TODO optimize } -length1.label = "Frequency"; +function nonempty2([, {size}]) { + return size > 0; +} function maybeInset(inset, inset1, inset2) { return inset === undefined && inset1 === undefined && inset2 === undefined diff --git a/test/output/aaplVolume.svg b/test/output/aaplVolume.svg index 577d34d89d..95ff42a9e1 100644 --- a/test/output/aaplVolume.svg +++ b/test/output/aaplVolume.svg @@ -2,40 +2,40 @@ - 0 + 0.00 - 2 + 0.02 - 4 + 0.04 - 6 + 0.06 - 8 + 0.08 - 10 + 0.10 - 12 + 0.12 - 14 + 0.14 - 16 - ↑ Frequency (%) + 0.16 + ↑ Frequency @@ -73,7 +73,7 @@ - + diff --git a/test/output/athletesHeightWeightBin.svg b/test/output/athletesHeightWeightBin.svg index af6b696d38..37cfa1ff86 100644 --- a/test/output/athletesHeightWeightBin.svg +++ b/test/output/athletesHeightWeightBin.svg @@ -43,7 +43,7 @@ 2.2 - ↑ height + @@ -73,7 +73,7 @@ 160 - weight → + diff --git a/test/output/athletesSexWeight.svg b/test/output/athletesSexWeight.svg index 09b3d81c5a..eb9a848e3e 100644 --- a/test/output/athletesSexWeight.svg +++ b/test/output/athletesSexWeight.svg @@ -66,7 +66,7 @@ 160 - weight → + diff --git a/test/output/athletesSportWeight.svg b/test/output/athletesSportWeight.svg index 46c6f6c084..bc376b136a 100644 --- a/test/output/athletesSportWeight.svg +++ b/test/output/athletesSportWeight.svg @@ -1,950 +1,1064 @@ - - aquatics + + aquatics - - archery + + archery - - athletics + + athletics - - badminton + + badminton - - basketball + + basketball - - canoe + + boxing - - cycling + + canoe - - equestrian + + cycling - - fencing + + equestrian - - football + + fencing - - golf + + football - - gymnastics + + golf - - handball + + gymnastics - - hockey + + handball - - judo + + hockey - - modern pentathlon + + judo - - rowing + + modern pentathlon - - rugby sevens + + rowing - - sailing + + rugby sevens - - shooting + + sailing - - table tennis + + shooting - - taekwondo + + table tennis - - tennis + + taekwondo - - triathlon + + tennis - - volleyball + + triathlon - - weightlifting + + volleyball - - wrestling + + weightlifting + + + wrestling sport - - 40 + 40 + - - 60 + 60 + - - 80 + 80 + - - 100 + 100 + - - 120 + 120 + - - 140 + 140 + - - 160 - weight → + 160 + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/athletesWeight.svg b/test/output/athletesWeight.svg index 7d0e98057e..0ad0d47e2a 100644 --- a/test/output/athletesWeight.svg +++ b/test/output/athletesWeight.svg @@ -70,7 +70,7 @@ 180 - weight → + diff --git a/test/output/athletesWeightCumulative.svg b/test/output/athletesWeightCumulative.svg new file mode 100644 index 0000000000..1aee9037e9 --- /dev/null +++ b/test/output/athletesWeightCumulative.svg @@ -0,0 +1,80 @@ + + + + 0 + + + 1,000 + + + 2,000 + + + 3,000 + + + 4,000 + + + 5,000 + + + 6,000 + + + 7,000 + + + 8,000 + + + 9,000 + + + 10,000 + ↑ Frequency + + + + 40 + + + 60 + + + 80 + + + 100 + + + 120 + + + 140 + + + 160 + + + 180 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/diamondsCaratPrice.svg b/test/output/diamondsCaratPrice.svg index 9c01b884f1..beac6813c3 100644 --- a/test/output/diamondsCaratPrice.svg +++ b/test/output/diamondsCaratPrice.svg @@ -56,7 +56,7 @@ 19,000 - ↑ price + @@ -88,7 +88,7 @@ 5.0 - carat → + diff --git a/test/output/penguinMassSpecies.svg b/test/output/penguinMassSpecies.svg index 44fed5a9db..6646a80bc0 100644 --- a/test/output/penguinMassSpecies.svg +++ b/test/output/penguinMassSpecies.svg @@ -4,41 +4,29 @@ 0 - + - 10 - - - - 20 - - - - 30 - - - - 40 + 50 - + - 50 + 100 - + - 60 + 150 - + - 70 + 200 - + - 80 + 250 - + - 90 + 300 ↑ Frequency @@ -71,22 +59,22 @@ Body mass (g) → - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/test/output/penguinSexMassCulmenSpecies.svg b/test/output/penguinSexMassCulmenSpecies.svg index 62c6c4f943..2a33385742 100644 --- a/test/output/penguinSexMassCulmenSpecies.svg +++ b/test/output/penguinSexMassCulmenSpecies.svg @@ -51,7 +51,7 @@ 58 - ↑ culmen_length_mm + @@ -158,7 +158,7 @@ 6k - body_mass_g → + diff --git a/test/output/uniformRandomDifference.svg b/test/output/uniformRandomDifference.svg index c5d402dda3..a3c47e0041 100644 --- a/test/output/uniformRandomDifference.svg +++ b/test/output/uniformRandomDifference.svg @@ -2,48 +2,48 @@ - 0 + 0.00 - 1 + 0.01 - 2 + 0.02 - 3 + 0.03 - 4 + 0.04 - 5 + 0.05 - 6 + 0.06 - 7 + 0.07 - 8 + 0.08 - 9 + 0.09 - 10 - ↑ Frequency (%) + 0.10 + ↑ Frequency @@ -83,17 +83,17 @@ - + - - + + - + - + diff --git a/test/plots/athletes-weight-cumulative.js b/test/plots/athletes-weight-cumulative.js new file mode 100644 index 0000000000..17612c9b7a --- /dev/null +++ b/test/plots/athletes-weight-cumulative.js @@ -0,0 +1,11 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const athletes = await d3.csv("data/athletes.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.rectY(athletes, Plot.binX({y: "count"}, {x: "weight", cumulative: true})) + ] + }); +} diff --git a/test/plots/index.js b/test/plots/index.js index bdd1ab8652..53962fa05e 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -9,6 +9,7 @@ export {default as athletesNationality} from "./athletes-nationality.js"; export {default as athletesSexWeight} from "./athletes-sex-weight.js"; export {default as athletesSportWeight} from "./athletes-sport-weight.js"; export {default as athletesWeight} from "./athletes-weight.js"; +export {default as athletesWeightCumulative} from "./athletes-weight-cumulative.js"; export {default as ballotStatusRace} from "./ballot-status-race.js"; export {default as beckerBarley} from "./becker-barley.js"; export {default as caltrain} from "./caltrain.js"; From 7bcff91adf5e46bcf69c197acbf0104a0327d2fb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 16:13:10 -0700 Subject: [PATCH 33/48] reorder export --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 7efca6372f..0b7294f1f0 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ export {Rect, rect, rectX, rectY} from "./marks/rect.js"; export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js"; export {Text, text, textX, textY} from "./marks/text.js"; export {TickX, TickY, tickX, tickY} from "./marks/tick.js"; -export {bin, binX, binY, binXMid, binYMid, binMid} from "./transforms/bin.js"; +export {bin, binMid, binX, binY, binXMid, binYMid} from "./transforms/bin.js"; export {group, groupX, groupY, groupZ} from "./transforms/group.js"; export {normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; From 12139793ea8571a57ed919f8c3024ed0fcad1842 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 16:19:32 -0700 Subject: [PATCH 34/48] no insets for binMid --- src/transforms/bin.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index bfec09292b..7f1bdcc211 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -36,23 +36,21 @@ export function binYMid(outputs, options = {}) { return {...transform, y: mid(y1, y2)}; } -// Group on {z, fill, stroke}, then bin on x and y. -export function binMid(outputs, options) { - const {x1, x2, y1, y2, ...transform} = bin(outputs, options); - return {...transform, x: mid(x1, x2), y: mid(y1, y2)}; // TODO don’t set insets -} - // Group on {z, fill, stroke}, then bin on x and y. export function bin(outputs, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) { - let {x, y} = options; - x = maybeBinValue(x, options); - y = maybeBinValue(y, options); - ([x.value, y.value] = maybeTuple(x.value, y.value)); + const {x, y} = maybeBinValueTuple(options); ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); return binn(x, y, null, null, outputs, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options}); } +// Group on {z, fill, stroke}, then bin on x and y. +export function binMid(outputs, options) { + const {x, y} = maybeBinValueTuple(options); + const {x1, x2, y1, y2, ...transform} = binn(x, y, null, null, outputs, options); + return {...transform, x: mid(x1, x2), y: mid(y1, y2)}; +} + function binn( bx, // optionally bin on x (exclusive with gx) by, // optionally bin on y (exclusive with gy) @@ -155,6 +153,14 @@ function maybeBinValue(value, {cumulative, domain, thresholds} = {}, defaultValu return value; } +function maybeBinValueTuple(options = {}) { + let {x, y} = options; + x = maybeBinValue(x, options); + y = maybeBinValue(y, options); + ([x.value, y.value] = maybeTuple(x.value, y.value)); + return {x, y}; +} + function maybeBin(options) { if (options == null) return; const {value, cumulative, domain, thresholds} = options; From 4f0ed8121e69cdf254d56e8341fce99b641f411d Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 16:26:17 -0700 Subject: [PATCH 35/48] restore labels --- src/transforms/bin.js | 6 ++++-- test/output/athletesHeightWeightBin.svg | 4 ++-- test/output/athletesSexWeight.svg | 2 +- test/output/athletesSportWeight.svg | 2 +- test/output/athletesWeight.svg | 2 +- test/output/athletesWeightCumulative.svg | 2 +- test/output/diamondsCaratPrice.svg | 4 ++-- test/output/penguinSexMassCulmenSpecies.svg | 4 ++-- 8 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 7f1bdcc211..121d86b072 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -1,6 +1,6 @@ import {bin as binner} from "d3"; import {firstof} from "../defined.js"; -import {valueof, range, identity, maybeLazyChannel, maybeTransform, maybeTuple, maybeColor, maybeValue, mid} from "../mark.js"; +import {valueof, range, identity, maybeLazyChannel, maybeTransform, maybeTuple, maybeColor, maybeValue, mid, labelof} from "../mark.js"; import {offset} from "../style.js"; import {maybeGroup, maybeGroupOutputs, reduceIdentity} from "./group.js"; @@ -164,7 +164,7 @@ function maybeBinValueTuple(options = {}) { function maybeBin(options) { if (options == null) return; const {value, cumulative, domain, thresholds} = options; - return data => { + const bin = data => { const V = valueof(data, value); const bin = binner().value(i => V[i]); if (domain !== undefined) bin.domain(domain); @@ -173,6 +173,8 @@ function maybeBin(options) { if (cumulative) bins = bins.map(bincumset); return bins.filter(nonempty2).map(binfilter); }; + bin.label = labelof(value); + return bin; } function binset(bin) { diff --git a/test/output/athletesHeightWeightBin.svg b/test/output/athletesHeightWeightBin.svg index 37cfa1ff86..af6b696d38 100644 --- a/test/output/athletesHeightWeightBin.svg +++ b/test/output/athletesHeightWeightBin.svg @@ -43,7 +43,7 @@ 2.2 - + ↑ height @@ -73,7 +73,7 @@ 160 - + weight → diff --git a/test/output/athletesSexWeight.svg b/test/output/athletesSexWeight.svg index eb9a848e3e..09b3d81c5a 100644 --- a/test/output/athletesSexWeight.svg +++ b/test/output/athletesSexWeight.svg @@ -66,7 +66,7 @@ 160 - + weight → diff --git a/test/output/athletesSportWeight.svg b/test/output/athletesSportWeight.svg index bc376b136a..7cfc77ed86 100644 --- a/test/output/athletesSportWeight.svg +++ b/test/output/athletesSportWeight.svg @@ -113,7 +113,7 @@ 160 - + weight → diff --git a/test/output/athletesWeight.svg b/test/output/athletesWeight.svg index 0ad0d47e2a..7d0e98057e 100644 --- a/test/output/athletesWeight.svg +++ b/test/output/athletesWeight.svg @@ -70,7 +70,7 @@ 180 - + weight → diff --git a/test/output/athletesWeightCumulative.svg b/test/output/athletesWeightCumulative.svg index 1aee9037e9..510f0ce85e 100644 --- a/test/output/athletesWeightCumulative.svg +++ b/test/output/athletesWeightCumulative.svg @@ -58,7 +58,7 @@ 180 - + weight → diff --git a/test/output/diamondsCaratPrice.svg b/test/output/diamondsCaratPrice.svg index beac6813c3..9c01b884f1 100644 --- a/test/output/diamondsCaratPrice.svg +++ b/test/output/diamondsCaratPrice.svg @@ -56,7 +56,7 @@ 19,000 - + ↑ price @@ -88,7 +88,7 @@ 5.0 - + carat → diff --git a/test/output/penguinSexMassCulmenSpecies.svg b/test/output/penguinSexMassCulmenSpecies.svg index 2a33385742..62c6c4f943 100644 --- a/test/output/penguinSexMassCulmenSpecies.svg +++ b/test/output/penguinSexMassCulmenSpecies.svg @@ -51,7 +51,7 @@ 58 - + ↑ culmen_length_mm @@ -158,7 +158,7 @@ 6k - + body_mass_g → From e37b196caab1ce6f188fe6fe66872c2da993d969 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 16:30:05 -0700 Subject: [PATCH 36/48] reverse cumulative --- src/transforms/bin.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 121d86b072..7a38f8d0c9 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -170,7 +170,10 @@ function maybeBin(options) { if (domain !== undefined) bin.domain(domain); if (thresholds !== undefined) bin.thresholds(thresholds); let bins = bin.value(i => V[i])(range(data)).map(binset); - if (cumulative) bins = bins.map(bincumset); + if (cumulative) { + if (cumulative < 0) bins.reverse(); + bins = bins.map(bincumset); + } return bins.filter(nonempty2).map(binfilter); }; bin.label = labelof(value); From 8bdfcd7ea09e1b60dcddb4b2f7b965cc3601d00c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 16:39:05 -0700 Subject: [PATCH 37/48] fix secondary group dimension --- src/transforms/bin.js | 2 +- test/plots/athletes-sport-weight.js | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 7a38f8d0c9..4d1bcffe30 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -117,7 +117,7 @@ function binn( for (const o of outputs) o.scope("facet", facet); for (const [, I] of maybeGroup(facet, G)) { for (const o of outputs) o.scope("z", I); - for (const [k, g] of maybeGroup(I, G)) { + for (const [k, g] of maybeGroup(I, K)) { for (const [x1, x2, fx] of BX) { const bb = fx(g); if (bb.length === 0) continue; diff --git a/test/plots/athletes-sport-weight.js b/test/plots/athletes-sport-weight.js index 00da2f9f20..b72f703101 100644 --- a/test/plots/athletes-sport-weight.js +++ b/test/plots/athletes-sport-weight.js @@ -5,6 +5,7 @@ export default async function() { const athletes = await d3.csv("data/athletes.csv", d3.autoType); return Plot.plot({ height: 640, + marginLeft: 100, x: { grid: true }, @@ -12,13 +13,8 @@ export default async function() { scheme: "YlGnBu", zero: true }, - facet: { - data: athletes, - marginLeft: 100, - y: "sport" - }, marks: [ - Plot.barX(athletes, Plot.binX({fill: "proportion-facet"}, {x: "weight", thresholds: 60})) + Plot.barX(athletes, Plot.binX({fill: "proportion-z"}, {x: "weight", y: "sport", thresholds: 60})) ] }); } From c3e05c5b53f3719e38c7abf4b8e0465599922ca6 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 16:43:27 -0700 Subject: [PATCH 38/48] proportion-group --- src/transforms/bin.js | 2 +- src/transforms/group.js | 4 ++-- test/plots/athletes-sport-weight.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 4d1bcffe30..48083dd145 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -116,8 +116,8 @@ function binn( const groupFacet = []; for (const o of outputs) o.scope("facet", facet); for (const [, I] of maybeGroup(facet, G)) { - for (const o of outputs) o.scope("z", I); for (const [k, g] of maybeGroup(I, K)) { + for (const o of outputs) o.scope("group", g); for (const [x1, x2, fx] of BX) { const bb = fx(g); if (bb.length === 0) continue; diff --git a/src/transforms/group.js b/src/transforms/group.js index e0c164a423..e76780b717 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -80,7 +80,7 @@ function groupn( const groupFacet = []; for (const o of outputs) o.scope("facet", facet); for (const [, I] of maybeGroup(facet, G)) { - for (const o of outputs) o.scope("z", I); + 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)) { groupFacet.push(i++); @@ -143,7 +143,7 @@ function maybeReduce(reduce, value) { case "sum": return value == null ? reduceCount : reduceSum; case "proportion": return reduceProportion(value, "data"); case "proportion-facet": return reduceProportion(value, "facet"); - case "proportion-z": return reduceProportion(value, "z"); + case "proportion-group": return reduceProportion(value, "group"); case "deviation": return reduceAccessor(deviation); case "min": return reduceAccessor(min); case "max": return reduceAccessor(max); diff --git a/test/plots/athletes-sport-weight.js b/test/plots/athletes-sport-weight.js index b72f703101..70040cf330 100644 --- a/test/plots/athletes-sport-weight.js +++ b/test/plots/athletes-sport-weight.js @@ -14,7 +14,7 @@ export default async function() { zero: true }, marks: [ - Plot.barX(athletes, Plot.binX({fill: "proportion-z"}, {x: "weight", y: "sport", thresholds: 60})) + Plot.barX(athletes, Plot.binX({fill: "proportion-group"}, {x: "weight", y: "sport", thresholds: 60})) ] }); } From d9ad9fa7fb2cbfc4a9f525e25c28abf98d044034 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 16:44:41 -0700 Subject: [PATCH 39/48] fix tests --- test/output/athletesSportWeight.svg | 1916 +++++++++++++-------------- 1 file changed, 901 insertions(+), 1015 deletions(-) diff --git a/test/output/athletesSportWeight.svg b/test/output/athletesSportWeight.svg index 7cfc77ed86..46c6f6c084 100644 --- a/test/output/athletesSportWeight.svg +++ b/test/output/athletesSportWeight.svg @@ -1,1064 +1,950 @@ - - aquatics + + aquatics - - archery + + archery - - athletics + + athletics - - badminton + + badminton - - basketball + + basketball - - boxing + + canoe - - canoe + + cycling - - cycling + + equestrian - - equestrian + + fencing - - fencing + + football - - football + + golf - - golf + + gymnastics - - gymnastics + + handball - - handball + + hockey - - hockey + + judo - - judo + + modern pentathlon - - modern pentathlon + + rowing - - rowing + + rugby sevens - - rugby sevens + + sailing - - sailing + + shooting - - shooting + + table tennis - - table tennis + + taekwondo - - taekwondo + + tennis - - tennis + + triathlon - - triathlon + + volleyball - - volleyball + + weightlifting - - weightlifting - - - wrestling + + wrestling sport - 40 - + + 40 - 60 - + + 60 - 80 - + + 80 - 100 - + + 100 - 120 - + + 120 - 140 - + + 140 - 160 - + + 160 weight → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 779fa076688c245c19b52632340ffce44aae0872 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 19:40:58 -0700 Subject: [PATCH 40/48] apply maybeReduce to data channel --- src/transforms/bin.js | 3 ++- src/transforms/group.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 48083dd145..bab2a32264 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -2,7 +2,7 @@ import {bin as binner} from "d3"; import {firstof} from "../defined.js"; import {valueof, range, identity, maybeLazyChannel, maybeTransform, maybeTuple, maybeColor, maybeValue, mid, labelof} from "../mark.js"; import {offset} from "../style.js"; -import {maybeGroup, maybeGroupOutputs, reduceIdentity} from "./group.js"; +import {maybeGroup, maybeGroupOutputs, maybeReduce, reduceIdentity} from "./group.js"; // Group on {z, fill, stroke}, then optionally on y, then bin x. export function binX(outputs, {inset, insetLeft, insetRight, ...options} = {}) { @@ -61,6 +61,7 @@ function binn( ) { bx = maybeBin(bx); by = maybeBin(by); + reduceData = maybeReduce(reduceData, identity); outputs = maybeGroupOutputs(outputs, inputs); // Produce x1, x2, y1, and y2 output channels as appropriate (when binning). diff --git a/src/transforms/group.js b/src/transforms/group.js index e76780b717..41a8a18e76 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -36,6 +36,7 @@ function groupn( {data: reduceData = reduceIdentity, ...outputs} = {}, // output channel definitions inputs = {} // input channels and options ) { + reduceData = maybeReduce(reduceData, identity); outputs = maybeGroupOutputs(outputs, inputs); // Produce x and y output channels as appropriate. @@ -133,7 +134,7 @@ export function maybeGroup(I, X) { return X ? sort(grouper(I, i => X[i]), first) : [[, I]]; } -function maybeReduce(reduce, value) { +export function maybeReduce(reduce, value) { if (reduce && typeof reduce.reduce === "function") return reduce; if (typeof reduce === "function") return reduceFunction(reduce); switch ((reduce + "").toLowerCase()) { From aa785ba1a49aa34a9694daccf19ab3e06150df5a Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 19:46:46 -0700 Subject: [PATCH 41/48] remove noop --- src/transforms/bin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index bab2a32264..52881854e7 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -170,7 +170,7 @@ function maybeBin(options) { const bin = binner().value(i => V[i]); if (domain !== undefined) bin.domain(domain); if (thresholds !== undefined) bin.thresholds(thresholds); - let bins = bin.value(i => V[i])(range(data)).map(binset); + let bins = bin(range(data)).map(binset); if (cumulative) { if (cumulative < 0) bins.reverse(); bins = bins.map(bincumset); From 4bcabf034c52d8d1beb0aefab665c200da2d29a9 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 19:47:18 -0700 Subject: [PATCH 42/48] shorten --- src/transforms/bin.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 52881854e7..cecf9bd325 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -171,10 +171,7 @@ function maybeBin(options) { if (domain !== undefined) bin.domain(domain); if (thresholds !== undefined) bin.thresholds(thresholds); let bins = bin(range(data)).map(binset); - if (cumulative) { - if (cumulative < 0) bins.reverse(); - bins = bins.map(bincumset); - } + if (cumulative) bins = (cumulative < 0 ? bins.reverse() : bins).map(bincumset); return bins.filter(nonempty2).map(binfilter); }; bin.label = labelof(value); From 1f70b729c997865e4355fad06c0f1e5e49bcc673 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 19:57:07 -0700 Subject: [PATCH 43/48] binned replaces input --- src/transforms/bin.js | 6 +-- test/output/penguinMassSpecies.svg | 68 ++++++++++++++++++------------ 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index cecf9bd325..f7375c1d84 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -77,7 +77,7 @@ function binn( // Greedily materialize the z, fill, and stroke channels (if channels and not // constants) so that we can reference them for subdividing groups without // computing them more than once. - const {z, fill, stroke, ...options} = inputs; + const {x, y, z, fill, stroke, ...options} = inputs; const [GZ, setGZ] = maybeLazyChannel(z); const [vfill] = maybeColor(fill); const [vstroke] = maybeColor(stroke); @@ -89,9 +89,9 @@ function binn( fill: GF, stroke: GS, ...options, + ...BX1 ? {x1: BX1, x2: BX2} : {x}, + ...BY1 ? {y1: BY1, y2: BY2} : {y}, ...GK && {[gk]: GK}, - ...BX1 && {x1: BX1, x2: BX2}, - ...BY1 && {y1: BY1, y2: BY2}, ...Object.fromEntries(outputs.map(({name, output}) => [name, output])), transform: maybeTransform(options, (data, facets) => { const K = valueof(data, k); diff --git a/test/output/penguinMassSpecies.svg b/test/output/penguinMassSpecies.svg index 6646a80bc0..44fed5a9db 100644 --- a/test/output/penguinMassSpecies.svg +++ b/test/output/penguinMassSpecies.svg @@ -4,29 +4,41 @@ 0 - + - 50 + 10 + + + + 20 + + + + 30 + + + + 40 - + - 100 + 50 - + - 150 + 60 - + - 200 + 70 - + - 250 + 80 - + - 300 + 90 ↑ Frequency @@ -59,22 +71,22 @@ Body mass (g) → - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + From 0e144f83fd7b1f300cec851467a853f4ff1564d5 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 20:14:04 -0700 Subject: [PATCH 44/48] =?UTF-8?q?don=E2=80=99t=20subgroup=20if=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/transforms/bin.js | 7 +- src/transforms/group.js | 14 +- test/output/seattleTemperatureCell.svg | 1504 ------------------------ 3 files changed, 14 insertions(+), 1511 deletions(-) delete mode 100644 test/output/seattleTemperatureCell.svg diff --git a/src/transforms/bin.js b/src/transforms/bin.js index f7375c1d84..4b8b6e2c74 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -1,8 +1,7 @@ import {bin as binner} from "d3"; -import {firstof} from "../defined.js"; import {valueof, range, identity, maybeLazyChannel, maybeTransform, maybeTuple, maybeColor, maybeValue, mid, labelof} from "../mark.js"; import {offset} from "../style.js"; -import {maybeGroup, maybeGroupOutputs, maybeReduce, reduceIdentity} from "./group.js"; +import {maybeGroup, maybeOutputs, maybeReduce, maybeSubgroup, reduceIdentity} from "./group.js"; // Group on {z, fill, stroke}, then optionally on y, then bin x. export function binX(outputs, {inset, insetLeft, insetRight, ...options} = {}) { @@ -62,7 +61,7 @@ function binn( bx = maybeBin(bx); by = maybeBin(by); reduceData = maybeReduce(reduceData, identity); - outputs = maybeGroupOutputs(outputs, inputs); + outputs = maybeOutputs(outputs, inputs); // Produce x1, x2, y1, and y2 output channels as appropriate (when binning). const [BX1, setBX1] = maybeLazyChannel(bx); @@ -98,7 +97,7 @@ function binn( const Z = valueof(data, z); const F = valueof(data, vfill); const S = valueof(data, vstroke); - const G = firstof(Z, F, S); + const G = maybeSubgroup(outputs, Z, F, S); const groupFacets = []; const groupData = []; const GK = K && setGK([]); diff --git a/src/transforms/group.js b/src/transforms/group.js index 41a8a18e76..cbdf6ad52d 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -37,7 +37,7 @@ function groupn( inputs = {} // input channels and options ) { reduceData = maybeReduce(reduceData, identity); - outputs = maybeGroupOutputs(outputs, inputs); + outputs = maybeOutputs(outputs, inputs); // Produce x and y output channels as appropriate. const [GX, setGX] = maybeLazyChannel(x); @@ -67,7 +67,7 @@ function groupn( const Z = valueof(data, z); const F = valueof(data, vfill); const S = valueof(data, vstroke); - const G = firstof(Z, F, S); + const G = maybeSubgroup(outputs, Z, F, S); const groupFacets = []; const groupData = []; const GX = X && setGX([]); @@ -102,7 +102,7 @@ function groupn( }; } -export function maybeGroupOutputs(outputs, inputs) { +export function maybeOutputs(outputs, inputs) { return Object.entries(outputs).map(([name, reduce]) => { const value = maybeInput(name, inputs); const reducer = maybeReduce(reduce, value); @@ -155,6 +155,14 @@ export function maybeReduce(reduce, value) { throw new Error("invalid reduce"); } +export function maybeSubgroup(outputs, Z, F, S) { + return firstof( + outputs.some(o => o.name === "z") ? undefined : Z, + outputs.some(o => o.name === "fill") ? undefined : F, + outputs.some(o => o.name === "stroke") ? undefined : S + ); +} + function reduceFunction(f) { return { reduce(I, X) { diff --git a/test/output/seattleTemperatureCell.svg b/test/output/seattleTemperatureCell.svg deleted file mode 100644 index 1a7724f216..0000000000 --- a/test/output/seattleTemperatureCell.svg +++ /dev/null @@ -1,1504 +0,0 @@ - - - - J - - - F - - - M - - - A - - - M - - - J - - - J - - - A - - - S - - - O - - - N - - - D - - - - - 1 - - - 2 - - - 3 - - - 4 - - - 5 - - - 6 - - - 7 - - - 8 - - - 9 - - - 10 - - - 11 - - - 12 - - - 13 - - - 14 - - - 15 - - - 16 - - - 17 - - - 18 - - - 19 - - - 20 - - - 21 - - - 22 - - - 23 - - - 24 - - - 25 - - - 26 - - - 27 - - - 28 - - - 29 - - - 30 - - - 31 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 7e52ab63b9499a650cc7e5bd55d1b6689c7cca45 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 20:15:06 -0700 Subject: [PATCH 45/48] restore test --- test/output/seattleTemperatureCell.svg | 503 +++++++++++++++++++++++++ 1 file changed, 503 insertions(+) create mode 100644 test/output/seattleTemperatureCell.svg diff --git a/test/output/seattleTemperatureCell.svg b/test/output/seattleTemperatureCell.svg new file mode 100644 index 0000000000..d15413161b --- /dev/null +++ b/test/output/seattleTemperatureCell.svg @@ -0,0 +1,503 @@ + + + + J + + + F + + + M + + + A + + + M + + + J + + + J + + + A + + + S + + + O + + + N + + + D + + + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + 8 + + + 9 + + + 10 + + + 11 + + + 12 + + + 13 + + + 14 + + + 15 + + + 16 + + + 17 + + + 18 + + + 19 + + + 20 + + + 21 + + + 22 + + + 23 + + + 24 + + + 25 + + + 26 + + + 27 + + + 28 + + + 29 + + + 30 + + + 31 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From f08e17f3840a5e2734c602377a0435365b5fc92b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 26 Mar 2021 20:33:56 -0700 Subject: [PATCH 46/48] scale percent --- src/axes.js | 3 +- src/facet.js | 2 +- src/plot.js | 6 ++-- src/scales/quantitative.js | 3 +- test/output/aaplVolume.svg | 20 ++++++------ .../mobyDickLetterRelativeFrequency.svg | 32 +++++++++---------- test/output/penguinSpeciesIslandRelative.svg | 24 +++++++------- test/output/uniformRandomDifference.svg | 30 ++++++++--------- test/output/wordLengthMobyDick.svg | 26 +++++++-------- test/plots/aapl-volume.js | 3 +- .../moby-dick-letter-relative-frequency.js | 3 +- test/plots/penguin-species-island-relative.js | 3 ++ test/plots/uniform-random-difference.js | 3 +- test/plots/word-length-moby-dick.js | 3 +- 14 files changed, 84 insertions(+), 77 deletions(-) diff --git a/src/axes.js b/src/axes.js index 0a959a8154..5e390e5d9e 100644 --- a/src/axes.js +++ b/src/axes.js @@ -102,10 +102,11 @@ function inferLabel(channels = [], scale, axis, key) { else if (candidate !== label) return; } if (candidate !== undefined) { - const {invert} = scale; + const {percent, invert} = scale; // Ignore the implicit label for temporal scales if it’s simply “date”. if (scale.type === "temporal" && /^(date|time|year)$/i.test(candidate)) return; if (scale.type !== "ordinal" && (key === "x" || key === "y")) { + if (percent) candidate = `${candidate} (%)`; if (axis.labelAnchor === "center") { candidate = `${candidate} →`; } else if (key === "x") { diff --git a/src/facet.js b/src/facet.js index 91ab781d49..52f60163f7 100644 --- a/src/facet.js +++ b/src/facet.js @@ -60,7 +60,7 @@ class Facet extends Mark { } const named = Object.create(null); for (const [name, channel] of channels) { - if (name !== undefined) named[name] = channel.value; + if (name !== undefined) Object.defineProperty(named, name, {get: () => channel.value}); // scale transform subchannels.push([undefined, channel]); } marksChannels.push(named); diff --git a/src/plot.js b/src/plot.js index cc6f2bce26..e32214a123 100644 --- a/src/plot.js +++ b/src/plot.js @@ -33,10 +33,8 @@ export function plot(options = {}) { const {scale} = channel; if (scale !== undefined) { const scaled = scaleChannels.get(scale); - const {transform} = options[scale] || {}; - if (transform !== undefined) { - channel.value = Array.from(channel.value, transform); - } + const {percent, transform = percent ? x => x * 100 : undefined} = options[scale] || {}; + if (transform !== undefined) channel.value = Array.from(channel.value, transform); if (scaled) scaled.push(channel); else scaleChannels.set(scale, [channel]); } diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 1bdde21862..d376c09229 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -137,6 +137,7 @@ export function ScaleQ(key, scale, channels, { clamp, zero, domain = (registry.get(key) === radius ? inferRadialDomain : inferDomain)(channels), + percent, round, range = registry.get(key) === radius ? inferRadialRange(channels, domain) : undefined, scheme, @@ -167,7 +168,7 @@ export function ScaleQ(key, scale, channels, { if (range !== undefined) scale.range(range); if (clamp) scale.clamp(clamp); - return {type: "quantitative", invert, domain, range, scale, inset}; + return {type: "quantitative", invert, domain, range, scale, inset, percent}; } export function ScaleLinear(key, channels, options) { diff --git a/test/output/aaplVolume.svg b/test/output/aaplVolume.svg index 95ff42a9e1..2c62b17b55 100644 --- a/test/output/aaplVolume.svg +++ b/test/output/aaplVolume.svg @@ -2,40 +2,40 @@ - 0.00 + 0 - 0.02 + 2 - 0.04 + 4 - 0.06 + 6 - 0.08 + 8 - 0.10 + 10 - 0.12 + 12 - 0.14 + 14 - 0.16 - ↑ Frequency + 16 + ↑ Frequency (%) diff --git a/test/output/mobyDickLetterRelativeFrequency.svg b/test/output/mobyDickLetterRelativeFrequency.svg index 7e4a35b6c9..98e728ad76 100644 --- a/test/output/mobyDickLetterRelativeFrequency.svg +++ b/test/output/mobyDickLetterRelativeFrequency.svg @@ -2,56 +2,56 @@ - 0.00 + 0 - 0.01 + 1 - 0.02 + 2 - 0.03 + 3 - 0.04 + 4 - 0.05 + 5 - 0.06 + 6 - 0.07 + 7 - 0.08 + 8 - 0.09 + 9 - 0.10 + 10 - 0.11 + 11 - 0.12 - ↑ Frequency + 12 + ↑ Frequency (%) @@ -136,7 +136,7 @@ - + @@ -152,7 +152,7 @@ - + diff --git a/test/output/penguinSpeciesIslandRelative.svg b/test/output/penguinSpeciesIslandRelative.svg index 64b8d4d54a..3a468ed694 100644 --- a/test/output/penguinSpeciesIslandRelative.svg +++ b/test/output/penguinSpeciesIslandRelative.svg @@ -1,38 +1,38 @@ - 0.0 + 0 - 0.1 + 10 - 0.2 + 20 - 0.3 + 30 - 0.4 + 40 - 0.5 + 50 - 0.6 + 60 - 0.7 + 70 - 0.8 + 80 - 0.9 + 90 - 1.0 - ↑ Frequency + 100 + ↑ Frequency (%) diff --git a/test/output/uniformRandomDifference.svg b/test/output/uniformRandomDifference.svg index a3c47e0041..d91cc21ba6 100644 --- a/test/output/uniformRandomDifference.svg +++ b/test/output/uniformRandomDifference.svg @@ -2,48 +2,48 @@ - 0.00 + 0 - 0.01 + 1 - 0.02 + 2 - 0.03 + 3 - 0.04 + 4 - 0.05 + 5 - 0.06 + 6 - 0.07 + 7 - 0.08 + 8 - 0.09 + 9 - 0.10 - ↑ Frequency + 10 + ↑ Frequency (%) @@ -85,18 +85,18 @@ - + - + - + diff --git a/test/output/wordLengthMobyDick.svg b/test/output/wordLengthMobyDick.svg index db719b1f0f..67cf1aca18 100644 --- a/test/output/wordLengthMobyDick.svg +++ b/test/output/wordLengthMobyDick.svg @@ -2,48 +2,48 @@ - 0.00 + 0 - 0.02 + 2 - 0.04 + 4 - 0.06 + 6 - 0.08 + 8 - 0.10 + 10 - 0.12 + 12 - 0.14 + 14 - 0.16 + 16 - 0.18 + 18 - 0.20 - ↑ Frequency + 20 + ↑ Frequency (%) @@ -95,7 +95,7 @@ - + diff --git a/test/plots/aapl-volume.js b/test/plots/aapl-volume.js index 5311d91da1..94930d6ba3 100644 --- a/test/plots/aapl-volume.js +++ b/test/plots/aapl-volume.js @@ -9,7 +9,8 @@ export default async function() { label: "Trade volume (log₁₀) →" }, y: { - grid: true + grid: true, + percent: true }, marks: [ Plot.rectY(data, Plot.binX({y: "proportion"}, {x: d => Math.log10(d.Volume)})), diff --git a/test/plots/moby-dick-letter-relative-frequency.js b/test/plots/moby-dick-letter-relative-frequency.js index 2a87c8df19..72e3a3da88 100644 --- a/test/plots/moby-dick-letter-relative-frequency.js +++ b/test/plots/moby-dick-letter-relative-frequency.js @@ -6,7 +6,8 @@ export default async function() { const letters = [...mobydick].filter(c => /[a-z]/i.test(c)).map(c => c.toUpperCase()); return Plot.plot({ y: { - grid: true + grid: true, + percent: true }, marks: [ Plot.barY(letters, Plot.groupX({y: "proportion"})), diff --git a/test/plots/penguin-species-island-relative.js b/test/plots/penguin-species-island-relative.js index 26e0e06c7f..eaad4dd503 100644 --- a/test/plots/penguin-species-island-relative.js +++ b/test/plots/penguin-species-island-relative.js @@ -4,6 +4,9 @@ import * as d3 from "d3"; export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ + y: { + percent: true + }, fx: { tickSize: 6 }, diff --git a/test/plots/uniform-random-difference.js b/test/plots/uniform-random-difference.js index ba60340fc1..242b9fe367 100644 --- a/test/plots/uniform-random-difference.js +++ b/test/plots/uniform-random-difference.js @@ -10,7 +10,8 @@ export default async function() { labelAnchor: "center" }, y: { - grid: true + grid: true, + percent: true }, marks: [ Plot.rectY({length: 10000}, Plot.binX({y: "proportion"}, {x: () => random() - random()})), diff --git a/test/plots/word-length-moby-dick.js b/test/plots/word-length-moby-dick.js index 9850ca6d48..15319707cc 100644 --- a/test/plots/word-length-moby-dick.js +++ b/test/plots/word-length-moby-dick.js @@ -17,7 +17,8 @@ export default async function() { labelAnchor: "right" }, y: { - grid: true + grid: true, + percent: true }, marks: [ Plot.barY(words, Plot.groupX({y: "proportion"}, {x: "length"})) From b3fc587fb9a4f1ae10d9c9a1e49553659d1eeac1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 27 Mar 2021 17:38:29 -0700 Subject: [PATCH 47/48] rm proportion-group --- src/transforms/bin.js | 1 - src/transforms/group.js | 2 -- test/plots/athletes-sport-weight.js | 8 ++++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 4b8b6e2c74..bd4c00c770 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -117,7 +117,6 @@ function binn( for (const o of outputs) o.scope("facet", facet); for (const [, I] of maybeGroup(facet, G)) { for (const [k, g] of maybeGroup(I, K)) { - for (const o of outputs) o.scope("group", g); for (const [x1, x2, fx] of BX) { const bb = fx(g); if (bb.length === 0) continue; diff --git a/src/transforms/group.js b/src/transforms/group.js index cbdf6ad52d..b715406bd3 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -81,7 +81,6 @@ function groupn( const groupFacet = []; for (const o of outputs) o.scope("facet", facet); for (const [, 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)) { groupFacet.push(i++); @@ -144,7 +143,6 @@ export function maybeReduce(reduce, value) { case "sum": return value == null ? reduceCount : reduceSum; case "proportion": return reduceProportion(value, "data"); case "proportion-facet": return reduceProportion(value, "facet"); - case "proportion-group": return reduceProportion(value, "group"); case "deviation": return reduceAccessor(deviation); case "min": return reduceAccessor(min); case "max": return reduceAccessor(max); diff --git a/test/plots/athletes-sport-weight.js b/test/plots/athletes-sport-weight.js index 70040cf330..00da2f9f20 100644 --- a/test/plots/athletes-sport-weight.js +++ b/test/plots/athletes-sport-weight.js @@ -5,7 +5,6 @@ export default async function() { const athletes = await d3.csv("data/athletes.csv", d3.autoType); return Plot.plot({ height: 640, - marginLeft: 100, x: { grid: true }, @@ -13,8 +12,13 @@ export default async function() { scheme: "YlGnBu", zero: true }, + facet: { + data: athletes, + marginLeft: 100, + y: "sport" + }, marks: [ - Plot.barX(athletes, Plot.binX({fill: "proportion-group"}, {x: "weight", y: "sport", thresholds: 60})) + Plot.barX(athletes, Plot.binX({fill: "proportion-facet"}, {x: "weight", thresholds: 60})) ] }); } From 5251f501086e582660e2e9059e64af815805b47f Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 27 Mar 2021 18:04:32 -0700 Subject: [PATCH 48/48] update snapshot --- test/output/athletesSportWeight.svg | 1916 ++++++++++++++------------- 1 file changed, 1015 insertions(+), 901 deletions(-) diff --git a/test/output/athletesSportWeight.svg b/test/output/athletesSportWeight.svg index 46c6f6c084..7cfc77ed86 100644 --- a/test/output/athletesSportWeight.svg +++ b/test/output/athletesSportWeight.svg @@ -1,950 +1,1064 @@ - - aquatics + + aquatics - - archery + + archery - - athletics + + athletics - - badminton + + badminton - - basketball + + basketball - - canoe + + boxing - - cycling + + canoe - - equestrian + + cycling - - fencing + + equestrian - - football + + fencing - - golf + + football - - gymnastics + + golf - - handball + + gymnastics - - hockey + + handball - - judo + + hockey - - modern pentathlon + + judo - - rowing + + modern pentathlon - - rugby sevens + + rowing - - sailing + + rugby sevens - - shooting + + sailing - - table tennis + + shooting - - taekwondo + + table tennis - - tennis + + taekwondo - - triathlon + + tennis - - volleyball + + triathlon - - weightlifting + + volleyball - - wrestling + + weightlifting + + + wrestling sport - - 40 + 40 + - - 60 + 60 + - - 80 + 80 + - - 100 + 100 + - - 120 + 120 + - - 140 + 140 + - - 160 + 160 + weight → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file