Skip to content

Commit

Permalink
Generic sort and filter transforms. (#205)
Browse files Browse the repository at this point in the history
* {filter, transform}

closes #138
supersedes #194

* generic sort, filter

* tolerate null

Co-authored-by: Philippe Rivière <[email protected]>
  • Loading branch information
mbostock and Fil authored Mar 9, 2021
1 parent 54e79fc commit 38c66a6
Show file tree
Hide file tree
Showing 10 changed files with 73 additions and 61 deletions.
65 changes: 44 additions & 21 deletions src/mark.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import {sort} from "d3-array";
import {color} from "d3-color";
import {nonempty} from "./defined.js";
import {ascendingDefined, 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) {
if (transform == null) transform = undefined;
constructor(data, channels = [], options = {}) {
const names = new Set();
this.data = arrayify(data);
this.transform = transform;
this.transform = maybeTransform(options);
this.channels = channels.filter(channel => {
const {name, value, optional} = channel;
if (value == null) {
Expand Down Expand Up @@ -133,15 +131,6 @@ export function maybeZero(x, x1, x2, x3 = identity) {
return [x1, x2];
}

// If a sort order is specified, returns a corresponding transform.
// TODO Allow the sort order to be specified as an array.
export function maybeSort(order) {
if (order !== undefined) {
if (typeof order !== "function") order = field(order);
return data => sort(data, 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} = {}) {
Expand Down Expand Up @@ -199,13 +188,12 @@ export function maybeLazyChannel(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);
};
export function maybeTransform({filter: f1, sort: s1, transform: t1} = {}, t2) {
if (t1 === undefined) {
if (f1 != null) t1 = filter(f1);
if (s1 != null) t1 = compose(t1, sort(s1));
}
return compose(t1, t2);
}

// Assuming that both x1 and x2 and lazy channels (per above), this derives a
Expand All @@ -226,3 +214,38 @@ export function mid(x1, x2) {
export function maybeValue(value) {
return typeof value === "undefined" || (value && value.toString === objectToString) ? value : {value};
}

function compose(t1, t2) {
if (t1 == null) return t2 === null ? undefined : t2;
if (t2 == null) return t1 === null ? undefined : t1;
return (data, index) => {
({data, index} = t1(data, index));
return t2(arrayify(data), index);
};
}

function sort(value) {
return (typeof value === "function" && value.length !== 1 ? sortCompare : sortValue)(value);
}

function sortCompare(compare) {
return (data, index) => {
const compareData = (i, j) => compare(data[i], data[j]);
return {data, index: index.map(I => I.slice().sort(compareData))};
};
}

function sortValue(value) {
return (data, index) => {
const V = valueof(data, value);
const compareValue = (i, j) => ascendingDefined(V[i], V[j]);
return {data, index: index.map(I => I.slice().sort(compareValue))};
};
}

function filter(value) {
return (data, index) => {
const V = valueof(data, value);
return {data, index: index.map(I => I.filter(i => V[i]))};
};
}
10 changes: 4 additions & 6 deletions src/marks/area.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {create} from "d3-selection";
import {area as shapeArea} from "d3-shape";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark, indexOf, maybeColor, maybeZero, maybeSort, first, second, titleGroup} from "../mark.js";
import {Mark, indexOf, maybeColor, maybeZero, first, second, titleGroup} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";

export class Area extends Mark {
Expand All @@ -20,9 +20,7 @@ export class Area extends Mark {
stroke,
curve,
tension,
sort,
transform = maybeSort(sort),
...style
...options
} = {}
) {
const [vfill, cfill] = maybeColor(fill, "currentColor");
Expand All @@ -41,14 +39,14 @@ export class Area extends Mark {
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
options
);
this.curve = Curve(curve, tension);
Style(this, {
fill: cfill,
stroke: cstroke,
strokeMiterlimit: cstroke === "none" ? undefined : 1,
...style
...options
});
}
render(I, {x, y, color}, {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z, title: L, fill: F, stroke: S}) {
Expand Down
7 changes: 3 additions & 4 deletions src/marks/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export class AbstractBar extends Mark {
insetLeft = inset,
rx,
ry,
transform,
...style
...options
} = {}
) {
const [vfill, cfill] = maybeColor(fill, "currentColor");
Expand All @@ -35,9 +34,9 @@ export class AbstractBar extends Mark {
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
options
);
Style(this, {fill: cfill, stroke: cstroke, ...style});
Style(this, {fill: cfill, stroke: cstroke, ...options});
this.insetTop = number(insetTop);
this.insetRight = number(insetRight);
this.insetBottom = number(insetBottom);
Expand Down
7 changes: 3 additions & 4 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export class Dot extends Mark {
title,
fill,
stroke,
transform,
...style
...options
} = {}
) {
const [vr, cr] = maybeNumber(r, 3);
Expand All @@ -33,14 +32,14 @@ export class Dot extends Mark {
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
options
);
this.r = cr;
Style(this, {
fill: cfill,
stroke: cstroke,
strokeWidth: cstroke === "none" ? undefined : 1.5,
...style
...options
});
}
render(
Expand Down
10 changes: 4 additions & 6 deletions src/marks/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {create} from "d3-selection";
import {line as shapeLine} from "d3-shape";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark, indexOf, identity, first, second, maybeColor, maybeSort, titleGroup} from "../mark.js";
import {Mark, indexOf, identity, first, second, maybeColor, titleGroup} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";

export class Line extends Mark {
Expand All @@ -18,9 +18,7 @@ export class Line extends Mark {
stroke,
curve,
tension,
sort,
transform = maybeSort(sort),
...style
...options
} = {}
) {
const [vfill, cfill] = maybeColor(fill, "none");
Expand All @@ -37,15 +35,15 @@ export class Line extends Mark {
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
options
);
this.curve = Curve(curve, tension);
Style(this, {
fill: cfill,
stroke: cstroke,
strokeWidth: cstroke === "none" ? undefined : 1.5,
strokeMiterlimit: cstroke === "none" ? undefined : 1,
...style
...options
});
}
render(I, {x, y, color}, {x: X, y: Y, z: Z, title: L, fill: F, stroke: S}) {
Expand Down
7 changes: 3 additions & 4 deletions src/marks/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export class Link extends Mark {
z,
title,
stroke,
transform,
...style
...options
} = {}
) {
const [vstroke, cstroke] = maybeColor(stroke, "currentColor");
Expand All @@ -31,9 +30,9 @@ export class Link extends Mark {
{name: "title", value: title, optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
options
);
Style(this, {stroke: cstroke, ...style});
Style(this, {stroke: cstroke, ...options});
}
render(
I,
Expand Down
7 changes: 3 additions & 4 deletions src/marks/rect.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ export class Rect extends Mark {
insetLeft = inset,
rx,
ry,
transform,
...style
...options
} = {}
) {
const [vfill, cfill] = maybeColor(fill, "currentColor");
Expand All @@ -41,9 +40,9 @@ export class Rect extends Mark {
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
options
);
Style(this, {fill: cfill, stroke: cstroke, ...style});
Style(this, {fill: cfill, stroke: cstroke, ...options});
this.insetTop = number(insetTop);
this.insetRight = number(insetRight);
this.insetBottom = number(insetBottom);
Expand Down
7 changes: 3 additions & 4 deletions src/marks/rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ export class RuleX extends Mark {
z,
title,
stroke,
transform,
...style
...options
} = {}
) {
const [vstroke, cstroke] = maybeColor(stroke, "currentColor");
Expand All @@ -29,9 +28,9 @@ export class RuleX extends Mark {
{name: "title", value: title, optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
options
);
Style(this, {stroke: cstroke, ...style});
Style(this, {stroke: cstroke, ...options});
}
render(
I,
Expand Down
7 changes: 3 additions & 4 deletions src/marks/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export class Text extends Mark {
text = indexOf,
title,
fill,
transform,
textAnchor,
fontFamily,
fontSize,
Expand All @@ -23,7 +22,7 @@ export class Text extends Mark {
fontWeight,
dx,
dy = "0.32em",
...style
...options
} = {}
) {
const [vfill, cfill] = maybeColor(fill, "currentColor");
Expand All @@ -37,9 +36,9 @@ export class Text extends Mark {
{name: "title", value: title, optional: true},
{name: "fill", value: vfill, scale: "color", optional: true}
],
transform
options
);
Style(this, {fill: cfill, ...style});
Style(this, {fill: cfill, ...options});
this.textAnchor = string(textAnchor);
this.fontFamily = string(fontFamily);
this.fontSize = string(fontSize);
Expand Down
7 changes: 3 additions & 4 deletions src/marks/tick.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ class AbstractTick extends Mark {
z,
title,
stroke,
transform,
...style
...options
} = {}
) {
const [vstroke, cstroke] = maybeColor(stroke, "currentColor");
Expand All @@ -25,9 +24,9 @@ class AbstractTick extends Mark {
{name: "title", value: title, optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
options
);
Style(this, {stroke: cstroke, ...style});
Style(this, {stroke: cstroke, ...options});
}
render(I, scales, channels, dimensions) {
const {color} = scales;
Expand Down

0 comments on commit 38c66a6

Please sign in to comment.