From 6a3012747fec01a463d9c0229a5673571c87d33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 9 Mar 2021 18:15:30 +0100 Subject: [PATCH 1/3] {filter, transform} closes #138 supersedes #194 --- src/mark.js | 8 ++++++-- src/marks/area.js | 3 ++- src/marks/bar.js | 3 ++- src/marks/dot.js | 3 ++- src/marks/line.js | 3 ++- src/marks/link.js | 3 ++- src/marks/rect.js | 3 ++- src/marks/rule.js | 3 ++- src/marks/text.js | 3 ++- src/marks/tick.js | 3 ++- 10 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/mark.js b/src/mark.js index a2aa0c38bd..fc98346596 100644 --- a/src/mark.js +++ b/src/mark.js @@ -7,8 +7,12 @@ 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 = [], {filter, transform} = {}) { + if (filter != null) transform = maybeTransform({transform}, (data, facets) => ({ + data, + index: facets.map(I => I.filter(i => filter(data[i], i, take(data, I)))) + })); + const names = new Set(); this.data = arrayify(data); this.transform = transform; diff --git a/src/marks/area.js b/src/marks/area.js index c9dd26d621..85c79d88ed 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -21,6 +21,7 @@ export class Area extends Mark { curve, tension, sort, + filter, transform = maybeSort(sort), ...style } = {} @@ -41,7 +42,7 @@ export class Area extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - transform + {filter, transform} ); this.curve = Curve(curve, tension); Style(this, { diff --git a/src/marks/bar.js b/src/marks/bar.js index 741629d2dc..4cf0eb1cab 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -20,6 +20,7 @@ export class AbstractBar extends Mark { insetLeft = inset, rx, ry, + filter, transform, ...style } = {} @@ -35,7 +36,7 @@ export class AbstractBar extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - transform + {filter, transform} ); Style(this, {fill: cfill, stroke: cstroke, ...style}); this.insetTop = number(insetTop); diff --git a/src/marks/dot.js b/src/marks/dot.js index 812303d219..0bb6485058 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -15,6 +15,7 @@ export class Dot extends Mark { title, fill, stroke, + filter, transform, ...style } = {} @@ -33,7 +34,7 @@ export class Dot extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - transform + {filter, transform} ); this.r = cr; Style(this, { diff --git a/src/marks/line.js b/src/marks/line.js index 8b83ef5a2b..f6b1738f59 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -19,6 +19,7 @@ export class Line extends Mark { curve, tension, sort, + filter, transform = maybeSort(sort), ...style } = {} @@ -37,7 +38,7 @@ export class Line extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - transform + {filter, transform} ); this.curve = Curve(curve, tension); Style(this, { diff --git a/src/marks/link.js b/src/marks/link.js index 8b74bcf5e8..fdeef9a176 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -15,6 +15,7 @@ export class Link extends Mark { z, title, stroke, + filter, transform, ...style } = {} @@ -31,7 +32,7 @@ export class Link extends Mark { {name: "title", value: title, optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - transform + {filter, transform} ); Style(this, {stroke: cstroke, ...style}); } diff --git a/src/marks/rect.js b/src/marks/rect.js index 1d41be45a0..de545b93a2 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -23,6 +23,7 @@ export class Rect extends Mark { insetLeft = inset, rx, ry, + filter, transform, ...style } = {} @@ -41,7 +42,7 @@ export class Rect extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - transform + {filter, transform} ); Style(this, {fill: cfill, stroke: cstroke, ...style}); this.insetTop = number(insetTop); diff --git a/src/marks/rule.js b/src/marks/rule.js index 922be78918..9253a5b9b4 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -14,6 +14,7 @@ export class RuleX extends Mark { z, title, stroke, + filter, transform, ...style } = {} @@ -29,7 +30,7 @@ export class RuleX extends Mark { {name: "title", value: title, optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - transform + {filter, transform} ); Style(this, {stroke: cstroke, ...style}); } diff --git a/src/marks/text.js b/src/marks/text.js index c07ca27680..263a72ab64 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -14,6 +14,7 @@ export class Text extends Mark { text = indexOf, title, fill, + filter, transform, textAnchor, fontFamily, @@ -37,7 +38,7 @@ export class Text extends Mark { {name: "title", value: title, optional: true}, {name: "fill", value: vfill, scale: "color", optional: true} ], - transform + {filter, transform} ); Style(this, {fill: cfill, ...style}); this.textAnchor = string(textAnchor); diff --git a/src/marks/tick.js b/src/marks/tick.js index dc7b1ef1b4..f9c48899cc 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -12,6 +12,7 @@ class AbstractTick extends Mark { z, title, stroke, + filter, transform, ...style } = {} @@ -25,7 +26,7 @@ class AbstractTick extends Mark { {name: "title", value: title, optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - transform + {filter, transform} ); Style(this, {stroke: cstroke, ...style}); } From ea30866920fce9533a693b82ad3982b0336bfc21 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 9 Mar 2021 09:53:51 -0800 Subject: [PATCH 2/3] generic sort, filter --- src/mark.js | 69 ++++++++++++++++++++++++++++++----------------- src/marks/area.js | 11 +++----- src/marks/bar.js | 8 +++--- src/marks/dot.js | 8 +++--- src/marks/line.js | 11 +++----- src/marks/link.js | 8 +++--- src/marks/rect.js | 8 +++--- src/marks/rule.js | 8 +++--- src/marks/text.js | 8 +++--- src/marks/tick.js | 8 +++--- 10 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/mark.js b/src/mark.js index fc98346596..3d8bced7f4 100644 --- a/src/mark.js +++ b/src/mark.js @@ -1,21 +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 = [], {filter, transform} = {}) { - if (filter != null) transform = maybeTransform({transform}, (data, facets) => ({ - data, - index: facets.map(I => I.filter(i => filter(data[i], i, take(data, I)))) - })); - + 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) { @@ -137,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} = {}) { @@ -203,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 !== undefined) t1 = filter(f1); + if (s1 !== undefined) t1 = compose(t1, sort(s1)); + } + return compose(t1, t2); } // Assuming that both x1 and x2 and lazy channels (per above), this derives a @@ -230,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 === undefined) return t2; + if (t2 === undefined) return 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 85c79d88ed..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,10 +20,7 @@ export class Area extends Mark { stroke, curve, tension, - sort, - filter, - transform = maybeSort(sort), - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "currentColor"); @@ -42,14 +39,14 @@ export class Area extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - {filter, 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 4cf0eb1cab..8122683b99 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -20,9 +20,7 @@ export class AbstractBar extends Mark { insetLeft = inset, rx, ry, - filter, - transform, - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "currentColor"); @@ -36,9 +34,9 @@ export class AbstractBar extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - {filter, 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 0bb6485058..56521d1cbb 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -15,9 +15,7 @@ export class Dot extends Mark { title, fill, stroke, - filter, - transform, - ...style + ...options } = {} ) { const [vr, cr] = maybeNumber(r, 3); @@ -34,14 +32,14 @@ export class Dot extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - {filter, 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 f6b1738f59..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,10 +18,7 @@ export class Line extends Mark { stroke, curve, tension, - sort, - filter, - transform = maybeSort(sort), - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "none"); @@ -38,7 +35,7 @@ export class Line extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - {filter, transform} + options ); this.curve = Curve(curve, tension); Style(this, { @@ -46,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 fdeef9a176..2ce84a9f2c 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -15,9 +15,7 @@ export class Link extends Mark { z, title, stroke, - filter, - transform, - ...style + ...options } = {} ) { const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); @@ -32,9 +30,9 @@ export class Link extends Mark { {name: "title", value: title, optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - {filter, 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 de545b93a2..e2f6f3bd94 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -23,9 +23,7 @@ export class Rect extends Mark { insetLeft = inset, rx, ry, - filter, - transform, - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "currentColor"); @@ -42,9 +40,9 @@ export class Rect extends Mark { {name: "fill", value: vfill, scale: "color", optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - {filter, 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 9253a5b9b4..062be86b5f 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -14,9 +14,7 @@ export class RuleX extends Mark { z, title, stroke, - filter, - transform, - ...style + ...options } = {} ) { const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); @@ -30,9 +28,9 @@ export class RuleX extends Mark { {name: "title", value: title, optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - {filter, 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 263a72ab64..0f0a3fc9e3 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -14,8 +14,6 @@ export class Text extends Mark { text = indexOf, title, fill, - filter, - transform, textAnchor, fontFamily, fontSize, @@ -24,7 +22,7 @@ export class Text extends Mark { fontWeight, dx, dy = "0.32em", - ...style + ...options } = {} ) { const [vfill, cfill] = maybeColor(fill, "currentColor"); @@ -38,9 +36,9 @@ export class Text extends Mark { {name: "title", value: title, optional: true}, {name: "fill", value: vfill, scale: "color", optional: true} ], - {filter, 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 f9c48899cc..11986b3894 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -12,9 +12,7 @@ class AbstractTick extends Mark { z, title, stroke, - filter, - transform, - ...style + ...options } = {} ) { const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); @@ -26,9 +24,9 @@ class AbstractTick extends Mark { {name: "title", value: title, optional: true}, {name: "stroke", value: vstroke, scale: "color", optional: true} ], - {filter, transform} + options ); - Style(this, {stroke: cstroke, ...style}); + Style(this, {stroke: cstroke, ...options}); } render(I, scales, channels, dimensions) { const {color} = scales; From 91bcbdc136997a87de1d8242a459f330d98d252f Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 9 Mar 2021 10:01:51 -0800 Subject: [PATCH 3/3] tolerate null --- src/mark.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mark.js b/src/mark.js index 3d8bced7f4..352838a40a 100644 --- a/src/mark.js +++ b/src/mark.js @@ -190,8 +190,8 @@ export function maybeLazyChannel(source) { // applies t1 and then applies t2. export function maybeTransform({filter: f1, sort: s1, transform: t1} = {}, t2) { if (t1 === undefined) { - if (f1 !== undefined) t1 = filter(f1); - if (s1 !== undefined) t1 = compose(t1, sort(s1)); + if (f1 != null) t1 = filter(f1); + if (s1 != null) t1 = compose(t1, sort(s1)); } return compose(t1, t2); } @@ -216,8 +216,8 @@ export function maybeValue(value) { } function compose(t1, t2) { - if (t1 === undefined) return t2; - if (t2 === undefined) return t1; + 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);