From 38c66a665f545c5b2b8a2664c949fed615d9dbf2 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 9 Mar 2021 10:34:17 -0800 Subject: [PATCH] Generic sort and filter transforms. (#205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * {filter, transform} closes #138 supersedes #194 * generic sort, filter * tolerate null Co-authored-by: Philippe Rivière --- src/mark.js | 65 ++++++++++++++++++++++++++++++++--------------- src/marks/area.js | 10 +++----- src/marks/bar.js | 7 +++-- src/marks/dot.js | 7 +++-- src/marks/line.js | 10 +++----- src/marks/link.js | 7 +++-- src/marks/rect.js | 7 +++-- src/marks/rule.js | 7 +++-- src/marks/text.js | 7 +++-- src/marks/tick.js | 7 +++-- 10 files changed, 73 insertions(+), 61 deletions(-) diff --git a/src/mark.js b/src/mark.js index a2aa0c38bd..352838a40a 100644 --- a/src/mark.js +++ b/src/mark.js @@ -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) { @@ -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} = {}) { @@ -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 @@ -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]))}; + }; +} diff --git a/src/marks/area.js b/src/marks/area.js index c9dd26d621..21147e6fb3 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -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 { @@ -20,9 +20,7 @@ export class Area extends Mark { stroke, curve, tension, - sort, - transform = maybeSort(sort), - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "currentColor"); @@ -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}) { diff --git a/src/marks/bar.js b/src/marks/bar.js index 741629d2dc..8122683b99 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -20,8 +20,7 @@ export class AbstractBar extends Mark { insetLeft = inset, rx, ry, - transform, - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "currentColor"); @@ -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); diff --git a/src/marks/dot.js b/src/marks/dot.js index 812303d219..56521d1cbb 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -15,8 +15,7 @@ export class Dot extends Mark { title, fill, stroke, - transform, - ...style + ...options } = {} ) { const [vr, cr] = maybeNumber(r, 3); @@ -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( diff --git a/src/marks/line.js b/src/marks/line.js index 8b83ef5a2b..37e257ef97 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -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 { @@ -18,9 +18,7 @@ export class Line extends Mark { stroke, curve, tension, - sort, - transform = maybeSort(sort), - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "none"); @@ -37,7 +35,7 @@ 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, { @@ -45,7 +43,7 @@ export class Line extends Mark { 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}) { diff --git a/src/marks/link.js b/src/marks/link.js index 8b74bcf5e8..2ce84a9f2c 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -15,8 +15,7 @@ export class Link extends Mark { z, title, stroke, - transform, - ...style + ...options } = {} ) { const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); @@ -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, diff --git a/src/marks/rect.js b/src/marks/rect.js index 1d41be45a0..e2f6f3bd94 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -23,8 +23,7 @@ export class Rect extends Mark { insetLeft = inset, rx, ry, - transform, - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "currentColor"); @@ -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); diff --git a/src/marks/rule.js b/src/marks/rule.js index 922be78918..062be86b5f 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -14,8 +14,7 @@ export class RuleX extends Mark { z, title, stroke, - transform, - ...style + ...options } = {} ) { const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); @@ -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, diff --git a/src/marks/text.js b/src/marks/text.js index c07ca27680..0f0a3fc9e3 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -14,7 +14,6 @@ export class Text extends Mark { text = indexOf, title, fill, - transform, textAnchor, fontFamily, fontSize, @@ -23,7 +22,7 @@ export class Text extends Mark { fontWeight, dx, dy = "0.32em", - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "currentColor"); @@ -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); diff --git a/src/marks/tick.js b/src/marks/tick.js index dc7b1ef1b4..11986b3894 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -12,8 +12,7 @@ class AbstractTick extends Mark { z, title, stroke, - transform, - ...style + ...options } = {} ) { const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); @@ -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;