Skip to content

Commit

Permalink
composable transform (observablehq#193)
Browse files Browse the repository at this point in the history
* composable transform

* fix crash on const z

* remove basic transform support

* normalize transform

* remove unused import

* movingAverage transform

* propagate computed channels

* stack transform

* preserve data during select

* fix select

* simpler composite transform

* fix select, normalize

* restore stack transform

* restore movingAverage transform

* minimize diff

* revert tests

* restore group transform

* fix tests

* restore bin transform

* fix label

* shorter

* cleaner

* cleaner bin

* more flexible rect[XY]

* z bin (observablehq#196)

* z bin

* z, x1 inheritance

* stacked bin test

* use rectY

* more TODO

* x1, y1 inheritance

* bins as grouped data

* fix x inheritance

* bin1 normalize

* simplify

* reorder

* bin-z for 2D bins (observablehq#198)

* generealize z-bin1 to z-bin2
example http://localhost:8008/?test=penguinSexMassCulmenSpecies

remove unused functions

* allow separate thresholds_x and thresholds_y
(most useful when we want to specify them as arrays of values)

* a grid to match the binning

* tweaks

Co-authored-by: Mike Bostock <mbostock@gmail.com>

Co-authored-by: Philippe Rivière <fil@rezo.net>

* format

* group1 sort by x

* group2

* rename

* consolidate

* fix observablehq#201; group domain option

Co-authored-by: Philippe Rivière <fil@rezo.net>
  • Loading branch information
mbostock and Fil authored Mar 8, 2021
1 parent 067a917 commit 8f9332b
Show file tree
Hide file tree
Showing 31 changed files with 1,203 additions and 732 deletions.
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {Text, text, textX, textY} from "./marks/text.js";
export {TickX, TickY, tickX, tickY} from "./marks/tick.js";
export {bin, binX, binY, binR} from "./transforms/bin.js";
export {group, groupX, groupY} from "./transforms/group.js";
export {normalizeX, normalizeY} from "./transforms/normalize.js";
export {movingAverageX, movingAverageY} from "./transforms/movingAverage.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";
93 changes: 48 additions & 45 deletions src/mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {nonempty} from "./defined.js";

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
const TypedArray = Object.getPrototypeOf(Uint8Array);
const objectToString = Object.prototype.toString;

export class Mark {
constructor(data, channels = [], transform = identity) {
constructor(data, channels = [], transform) {
if (transform == null) transform = undefined;
const names = new Set();
this.data = arrayify(data);
this.transform = transform;
Expand All @@ -29,44 +31,13 @@ export class Mark {
});
}
initialize(facets) {
let index, data;
if (this.data !== undefined) {
if (this.transform === identity) { // optimized common case
data = this.data, index = facets !== undefined ? facets : range(data);
} else if (this.transform.length === 2) { // facet-aware transform
({index, data} = this.transform(this.data, facets));
data = arrayify(data);
} else if (facets !== undefined) { // basic transform, faceted
// Apply the transform to each facet’s data separately; since the
// transformed data can have different cardinality than the source
// data, also build up a new faceted index into the transformed data.
// Note that the transformed data must be a generic Array, not a typed
// array, for the array.flat() call to flatten the array.
let k = 0;
index = [], data = [];
for (const facet of facets) {
const facetData = arrayify(this.transform(take(this.data, facet)), Array);
const facetIndex = facetData === undefined ? undefined : offsetRange(facetData, k);
k += facetData.length;
index.push(facetIndex);
data.push(facetData);
}
data = data.flat();
// Reorder any channel value arrays to match the transformed index.
// Since there may be zero or multiple channels that need reordering,
// we lazily compute the flattened transformed index.
let facetIndex;
for (const channel of this.channels) {
let {value} = channel;
if (typeof value !== "function") {
if (facetIndex === undefined) facetIndex = facets.flat();
channel.value = take(arrayify(value), facetIndex);
}
}
} else { // basic transform, non-faceted
data = arrayify(this.transform(this.data));
index = data === undefined ? undefined : range(data);
}
let data = this.data;
let index = facets === undefined && data != null ? range(data) : facets;
if (data !== undefined && this.transform !== undefined) {
if (facets === undefined) index = index.length ? [index] : [];
({index, data} = this.transform(data, index));
data = arrayify(data);
if (facets === undefined && index.length) ([index] = index);
}
return {
index,
Expand Down Expand Up @@ -171,6 +142,14 @@ export function maybeSort(order) {
}
}

// A helper for extracting the z channel, if it is variable. Used by transforms
// that require series, such as moving average and normalize.
export function maybeZ({z, fill, stroke} = {}) {
if (z === undefined) ([z] = maybeColor(fill));
if (z === undefined) ([z] = maybeColor(stroke));
return z;
}

// Applies the specified titles via selection.call.
export function title(L) {
return L ? selection => selection
Expand All @@ -192,12 +171,6 @@ export function range(data) {
return Uint32Array.from(data, indexOf);
}

// Returns a Uint32Array with elements [k, k + 1, … k + data.length - 1].
export function offsetRange(data, k) {
k = Math.floor(k);
return Uint32Array.from(data, (_, i) => i + k);
}

// Returns an array [values[index[0]], values[index[1]], …].
export function take(values, index) {
return Array.from(index, i => values[i]);
Expand All @@ -223,3 +196,33 @@ export function lazyChannel(source) {
export function maybeLazyChannel(source) {
return source == null ? [] : lazyChannel(source);
}

// If both t1 and t2 are defined, returns a composite transform that first
// applies t1 and then applies t2.
export function maybeTransform({transform: t1} = {}, t2) {
if (t1 === undefined) return t2;
if (t2 === undefined) return t1;
return (data, index) => {
({data, index} = t1(data, index));
return t2(arrayify(data), index);
};
}

// Assuming that both x1 and x2 and lazy channels (per above), this derives a
// new a channel that’s the average of the two, and which inherits the channel
// label (if any).
export function mid(x1, x2) {
return {
transform(data) {
const X1 = x1.transform(data);
const X2 = x2.transform(data);
return Float64Array.from(X1, (_, i) => (X1[i] + X2[i]) / 2);
},
label: x1.label
};
}

// This distinguishes between per-dimension options and a standalone value.
export function maybeValue(value) {
return typeof value === "undefined" || (value && value.toString === objectToString) ? value : {value};
}
6 changes: 3 additions & 3 deletions src/marks/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import {rect, rectX, rectY} from "./rect.js";
import {dot} from "./dot.js";

export function binRect(data, options) {
return rect(data, bin({insetTop: 1, insetLeft: 1, ...options, out: "fill"}));
return rect(data, bin({...options, out: "fill"}));
}

export function binDot(data, options) {
return dot(data, binR(options));
}

export function binRectY(data, options) {
return rectY(data, binX({insetLeft: 1, ...options}));
return rectY(data, binX(options));
}

export function binRectX(data, options) {
return rectX(data, binY({insetTop: 1, ...options}));
return rectX(data, binY(options));
}
13 changes: 7 additions & 6 deletions src/marks/rect.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {ascending} from "d3-array";
import {create} from "d3-selection";
import {zero} from "../mark.js";
import {filter} from "../defined.js";
import {Mark, number, maybeColor, title} from "../mark.js";
import {Mark, number, maybeColor, maybeZero, title} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";

export class Rect extends Mark {
Expand Down Expand Up @@ -84,10 +83,12 @@ export function rect(data, options) {
return new Rect(data, options);
}

export function rectX(data, {x, y1, y2, ...options} = {}) {
return new Rect(data, {...options, x1: zero, x2: x, y1, y2});
export function rectX(data, {x, x1, x2, y1, y2, ...options} = {}) {
([x1, x2] = maybeZero(x, x1, x2));
return new Rect(data, {...options, x1, x2, y1, y2});
}

export function rectY(data, {x1, x2, y, ...options} = {}) {
return new Rect(data, {...options, x1, x2, y1: zero, y2: y});
export function rectY(data, {x1, x2, y, y1, y2, ...options} = {}) {
([y1, y2] = maybeZero(y, y1, y2));
return new Rect(data, {...options, x1, x2, y1, y2});
}
Loading

0 comments on commit 8f9332b

Please sign in to comment.