From 4bc2405be3c95a7cffc8373c54358b6ca03cff97 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 28 May 2023 10:23:38 -0700 Subject: [PATCH 1/7] exclusiveFacets --- src/transforms/exclusiveFacets.js | 39 +++++++++++++++++++++++++++++++ src/transforms/stack.js | 2 ++ 2 files changed, 41 insertions(+) create mode 100644 src/transforms/exclusiveFacets.js diff --git a/src/transforms/exclusiveFacets.js b/src/transforms/exclusiveFacets.js new file mode 100644 index 0000000000..f02ccd231a --- /dev/null +++ b/src/transforms/exclusiveFacets.js @@ -0,0 +1,39 @@ +import {slice} from "../options.js"; + +// TODO How to reindex channels supplied as arrays? I don’t want to inspect +// arbitrary values on the options; maybe we could use this.channels? +export function exclusiveFacets(data, facets) { + if (facets.length === 1) return {data, facets}; // only one facet; trivially exclusive + + const n = data.length; + const O = new Uint8Array(n); + let overlaps = 0; + + // Count the number of overlapping indexes across facets. + for (const facet of facets) { + for (const i of facet) { + if (O[i]) ++overlaps; + O[i] = 1; + } + } + + // Do nothing if the facets are already exclusive. + if (overlaps === 0) return {data, facets}; // facets are exclusive + + // For each overlapping index (duplicate), assign a new unique index at the + // end of the existing array. For example, [[0, 1, 2], [2, 1, 3]] would become + // [[0, 1, 2], [4, 5, 3]]. Attach a plan to the facets array, to be able to + // read the values associated with the old index in unaffected channels. + facets = facets.map((facet) => slice(facet, Uint32Array)); + let j = n; + O.fill(0); + for (const facet of facets) { + for (let k = 0, m = facet.length; k < m; ++k) { + const i = facet[k]; + if (O[i]) facet[k] = j++; + O[i] = 1; + } + } + + return {data, facets}; +} diff --git a/src/transforms/stack.js b/src/transforms/stack.js index 25d676fe2f..9c96997af0 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -4,6 +4,7 @@ import {withTip} from "../mark.js"; import {maybeApplyInterval, maybeColumn, maybeZ, maybeZero} from "../options.js"; import {column, field, mid, one, range, valueof} from "../options.js"; import {basic} from "./basic.js"; +import {exclusiveFacets} from "./exclusiveFacets.js"; export function stackX(stackOptions = {}, options = {}) { if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); @@ -85,6 +86,7 @@ function stack(x, y = one, kx, ky, {offset, order, reverse}, options) { order = maybeOrder(order, offset, ky); return [ basic(options, (data, facets, plotOptions) => { + ({data, facets} = exclusiveFacets(data, facets)); const X = x == null ? undefined : setX(maybeApplyInterval(valueof(data, x), plotOptions?.[kx])); const Y = valueof(data, y, Float64Array); const Z = valueof(data, z); From fc59caeb2f1476cd782e83b99b63964b925fb223 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 28 May 2023 10:34:15 -0700 Subject: [PATCH 2/7] pad the data with duplicates --- src/transforms/exclusiveFacets.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transforms/exclusiveFacets.js b/src/transforms/exclusiveFacets.js index f02ccd231a..a96c69ac8b 100644 --- a/src/transforms/exclusiveFacets.js +++ b/src/transforms/exclusiveFacets.js @@ -21,16 +21,16 @@ export function exclusiveFacets(data, facets) { if (overlaps === 0) return {data, facets}; // facets are exclusive // For each overlapping index (duplicate), assign a new unique index at the - // end of the existing array. For example, [[0, 1, 2], [2, 1, 3]] would become - // [[0, 1, 2], [4, 5, 3]]. Attach a plan to the facets array, to be able to - // read the values associated with the old index in unaffected channels. + // end of the existing array, duplicating the datum. For example, [[0, 1, 2], + // [2, 1, 3]] would become [[0, 1, 2], [4, 5, 3]]. + data = slice(data); facets = facets.map((facet) => slice(facet, Uint32Array)); let j = n; O.fill(0); for (const facet of facets) { for (let k = 0, m = facet.length; k < m; ++k) { const i = facet[k]; - if (O[i]) facet[k] = j++; + if (O[i]) (facet[k] = j), (data[j] = data[i]), ++j; O[i] = 1; } } From ce3b378368f2f88c1509154ce947ca7abb845f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 1 Jun 2023 09:19:38 +0200 Subject: [PATCH 3/7] reindex --- src/options.js | 2 ++ src/transforms/exclusiveFacets.js | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/options.js b/src/options.js index f70a118ef8..7eb6c659e2 100644 --- a/src/options.js +++ b/src/options.js @@ -17,6 +17,8 @@ export function valueof(data, value, type) { ? map(data, constant(value), type) : typeof value?.transform === "function" ? maybeTypedArrayify(value.transform(data), type) + : value && data?.reindex + ? maybeTypedMap(data.reindex, (i) => value[i], type) : maybeTypedArrayify(value, type); } diff --git a/src/transforms/exclusiveFacets.js b/src/transforms/exclusiveFacets.js index a96c69ac8b..7c0443ea6f 100644 --- a/src/transforms/exclusiveFacets.js +++ b/src/transforms/exclusiveFacets.js @@ -24,13 +24,16 @@ export function exclusiveFacets(data, facets) { // end of the existing array, duplicating the datum. For example, [[0, 1, 2], // [2, 1, 3]] would become [[0, 1, 2], [4, 5, 3]]. data = slice(data); + // Attach a reindex map to the data, to interpret channels specified as arrays. + data.reindex = new Uint32Array(n + overlaps); facets = facets.map((facet) => slice(facet, Uint32Array)); let j = n; O.fill(0); for (const facet of facets) { for (let k = 0, m = facet.length; k < m; ++k) { const i = facet[k]; - if (O[i]) (facet[k] = j), (data[j] = data[i]), ++j; + if (O[i]) (facet[k] = j), (data[j] = data[i]), (data.reindex[j] = i), ++j; + else data.reindex[i] = i; O[i] = 1; } } From 6111cd7e19943c04a44a2ab17c4974aca35bada0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 1 Jun 2023 09:47:36 +0200 Subject: [PATCH 4/7] test --- test/output/facetReindex.svg | 1136 ++++++++++++++++++++++++++++++++++ test/plots/facet-reindex.ts | 34 + test/plots/index.ts | 1 + 3 files changed, 1171 insertions(+) create mode 100644 test/output/facetReindex.svg create mode 100644 test/plots/facet-reindex.ts diff --git a/test/output/facetReindex.svg b/test/output/facetReindex.svg new file mode 100644 index 0000000000..db90254da3 --- /dev/null +++ b/test/output/facetReindex.svg @@ -0,0 +1,1136 @@ + + + + + Biscoe + + + Dream + + + Torgersen + + + + facet value + + + + + + + + + + + + + + + + + + exclude + include + + + exclude + include + + + exclude + include + + + + facet option + + + + + + + + + + + + + + + 0 + 50 + 100 + 150 + 200 + 250 + 300 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/facet-reindex.ts b/test/plots/facet-reindex.ts new file mode 100644 index 0000000000..1d77563861 --- /dev/null +++ b/test/plots/facet-reindex.ts @@ -0,0 +1,34 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function facetReindex() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + const island = Plot.valueof(penguins, "island"); + return Plot.plot({ + width: 830, + marginLeft: 74, + marginRight: 68, + height: 130, + x: {domain: [0, penguins.length], round: true}, + y: {label: "facet option", axis: "right"}, + facet: {data: penguins, y: island}, + fy: {label: "facet value"}, + marks: [ + Plot.barX(penguins, { + facet: "exclude", + fill: island, // array channel to be reindexed + x: 1, + y: () => "exclude", + fillOpacity: 0.5, + insetRight: 0.5 + }), + Plot.barX(penguins, { + facet: "include", + fill: island, + x: 1, + y: () => "include" + }), + Plot.frame() + ] + }); +} diff --git a/test/plots/index.ts b/test/plots/index.ts index 65c6cfb5c6..82b3c2e88a 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -80,6 +80,7 @@ export * from "./empty-legend.js"; export * from "./empty-x.js"; export * from "./empty.js"; export * from "./energy-production.js"; +export * from "./facet-reindex.js"; export * from "./faithful-density-1d.js"; export * from "./faithful-density.js"; export * from "./federal-funds.js"; From daeb7d40f2ca98fc2868266bd59f00338c1a5f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 1 Jun 2023 09:48:18 +0200 Subject: [PATCH 5/7] done --- src/transforms/exclusiveFacets.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/transforms/exclusiveFacets.js b/src/transforms/exclusiveFacets.js index 7c0443ea6f..f186830dfb 100644 --- a/src/transforms/exclusiveFacets.js +++ b/src/transforms/exclusiveFacets.js @@ -1,7 +1,5 @@ import {slice} from "../options.js"; -// TODO How to reindex channels supplied as arrays? I don’t want to inspect -// arbitrary values on the options; maybe we could use this.channels? export function exclusiveFacets(data, facets) { if (facets.length === 1) return {data, facets}; // only one facet; trivially exclusive From c7f9f69703f6127c85acb89b6c89716849db955b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 18 Aug 2023 13:51:57 -0700 Subject: [PATCH 6/7] reindex iterables --- src/options.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/options.js b/src/options.js index 7eb6c659e2..e52732fde5 100644 --- a/src/options.js +++ b/src/options.js @@ -17,9 +17,11 @@ export function valueof(data, value, type) { ? map(data, constant(value), type) : typeof value?.transform === "function" ? maybeTypedArrayify(value.transform(data), type) - : value && data?.reindex - ? maybeTypedMap(data.reindex, (i) => value[i], type) - : maybeTypedArrayify(value, type); + : maybeTake(maybeTypedArrayify(value, type), data?.reindex); +} + +function maybeTake(values, index) { + return index ? take(values, index) : values; } function maybeTypedMap(data, f, type) { @@ -172,6 +174,7 @@ export function isScaleOptions(option) { // Disambiguates an options object (e.g., {y: "x2"}) from a channel value // definition expressed as a channel transform (e.g., {transform: …}). +// TODO Check typeof option[Symbol.iterator] !== "function"? export function isOptions(option) { return isObject(option) && typeof option.transform !== "function"; } @@ -225,7 +228,7 @@ export function where(data, test) { // Returns an array [values[index[0]], values[index[1]], …]. export function take(values, index) { - return map(index, (i) => values[i]); + return map(index, (i) => values[i], values.constructor); } // If f does not take exactly one argument, wraps it in a function that uses take. From ad8600b617225de251dfc1eefe69cc002c4c23e6 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 18 Aug 2023 13:58:04 -0700 Subject: [PATCH 7/7] reindex symbol --- src/options.js | 6 +++++- src/transforms/exclusiveFacets.js | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/options.js b/src/options.js index e52732fde5..3fdcbd8bff 100644 --- a/src/options.js +++ b/src/options.js @@ -7,6 +7,10 @@ import {maybeTimeInterval, maybeUtcInterval} from "./time.js"; export const TypedArray = Object.getPrototypeOf(Uint8Array); const objectToString = Object.prototype.toString; +// If a reindex is attached to the data, channel values expressed as arrays will +// be reindexed when the channels are instantiated. See exclusiveFacets. +export const reindex = Symbol("reindex"); + export function valueof(data, value, type) { const valueType = typeof value; return valueType === "string" @@ -17,7 +21,7 @@ export function valueof(data, value, type) { ? map(data, constant(value), type) : typeof value?.transform === "function" ? maybeTypedArrayify(value.transform(data), type) - : maybeTake(maybeTypedArrayify(value, type), data?.reindex); + : maybeTake(maybeTypedArrayify(value, type), data?.[reindex]); } function maybeTake(values, index) { diff --git a/src/transforms/exclusiveFacets.js b/src/transforms/exclusiveFacets.js index f186830dfb..facf94bfb8 100644 --- a/src/transforms/exclusiveFacets.js +++ b/src/transforms/exclusiveFacets.js @@ -1,4 +1,4 @@ -import {slice} from "../options.js"; +import {reindex, slice} from "../options.js"; export function exclusiveFacets(data, facets) { if (facets.length === 1) return {data, facets}; // only one facet; trivially exclusive @@ -20,18 +20,18 @@ export function exclusiveFacets(data, facets) { // For each overlapping index (duplicate), assign a new unique index at the // end of the existing array, duplicating the datum. For example, [[0, 1, 2], - // [2, 1, 3]] would become [[0, 1, 2], [4, 5, 3]]. + // [2, 1, 3]] would become [[0, 1, 2], [4, 5, 3]]. Also attach a reindex to + // the data to preserve the association of channel values specified as arrays. data = slice(data); - // Attach a reindex map to the data, to interpret channels specified as arrays. - data.reindex = new Uint32Array(n + overlaps); + const R = (data[reindex] = new Uint32Array(n + overlaps)); facets = facets.map((facet) => slice(facet, Uint32Array)); let j = n; O.fill(0); for (const facet of facets) { for (let k = 0, m = facet.length; k < m; ++k) { const i = facet[k]; - if (O[i]) (facet[k] = j), (data[j] = data[i]), (data.reindex[j] = i), ++j; - else data.reindex[i] = i; + if (O[i]) (facet[k] = j), (data[j] = data[i]), (R[j] = i), ++j; + else R[i] = i; O[i] = 1; } }