Skip to content

Commit

Permalink
exclusiveFacets (observablehq#1649)
Browse files Browse the repository at this point in the history
* exclusiveFacets

* pad the data with duplicates

* reindex

* test

* done

* reindex iterables

* reindex symbol

---------

Co-authored-by: Philippe Rivière <[email protected]>
  • Loading branch information
2 people authored and chaichontat committed Jan 14, 2024
1 parent 863c611 commit 6a80d91
Show file tree
Hide file tree
Showing 6 changed files with 1,224 additions and 2 deletions.
13 changes: 11 additions & 2 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -17,7 +21,11 @@ export function valueof(data, value, type) {
? map(data, constant(value), type)
: typeof value?.transform === "function"
? maybeTypedArrayify(value.transform(data), 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) {
Expand Down Expand Up @@ -170,6 +178,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";
}
Expand Down Expand Up @@ -223,7 +232,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.
Expand Down
40 changes: 40 additions & 0 deletions src/transforms/exclusiveFacets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {reindex, slice} from "../options.js";

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, duplicating the datum. For example, [[0, 1, 2],
// [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);
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]), (R[j] = i), ++j;
else R[i] = i;
O[i] = 1;
}
}

return {data, facets};
}
2 changes: 2 additions & 0 deletions src/transforms/stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 6a80d91

Please sign in to comment.