diff --git a/src/facet.js b/src/facet.js index fc26316aa8..9320b99e38 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,7 +1,4 @@ -import {column, maybeColorChannel, maybeNumberChannel, slice, valueof} from "./options.js"; -import {maybeSymbolChannel} from "./symbols.js"; -import {maybeFontSizeChannel} from "./marks/text.js"; -import {maybePathChannel} from "./marks/image.js"; +import {column, slice, valueof} from "./options.js"; export function facetReindex(facets, n) { if (facets.length === 1) return {facets}; @@ -54,44 +51,46 @@ export function maybeExpand(X, plan) { // Iterate over the options and pull out any that represent columns of values. const knownChannels = [ - ["x"], - ["x1"], - ["x2"], - ["y"], - ["y1"], - ["y2"], - ["z"], - ["ariaLabel"], - ["href"], - ["title"], - ["fill", (value) => maybeColorChannel(value)[0]], - ["stroke", (value) => maybeColorChannel(value)[0]], - ["fillOpacity", (value) => maybeNumberChannel(value)[0]], - ["strokeOpacity", (value) => maybeNumberChannel(value)[0]], - ["opacity", (value) => maybeNumberChannel(value)[0]], - ["strokeWidth", (value) => maybeNumberChannel(value)[0]], - ["symbol", (value) => maybeSymbolChannel(value)[0]], // dot - ["r", (value) => maybeNumberChannel(value)[0]], // dot - ["rotate", (value) => maybeNumberChannel(value)[0]], // dot, text - ["fontSize", (value) => maybeFontSizeChannel(value)[0]], // text - ["text"], // text - ["length", (value) => maybeNumberChannel(value)[0]], // vector - ["width", (value) => maybeNumberChannel(value)[0]], // image - ["height", (value) => maybeNumberChannel(value)[0]], // image - ["src", (value) => maybePathChannel(value)[0]], // image - ["weight", (value) => maybeNumberChannel(value)[0]] // density + "x", + "x1", + "x2", + "y", + "y1", + "y2", + "z", + "ariaLabel", + "href", + "title", + "fill", + "stroke", + "fillOpacity", + "strokeOpacity", + "opacity", + "strokeWidth", + "symbol", // dot + "r", // dot + "rotate", // dot, text + "fontSize", // text + "text", // text + "length", // vector + "width", // image + "height", // image + "src", // image + "weight" // density ]; -export function maybeExpandOutputs(options) { - const other = {}; - const outputs = []; - for (const [name, test = (value) => value] of knownChannels) { - const value = test(options[name]); +export function maybeExpandChannels(options) { + const channels = {}; + const [{transform: plan}, setPlan] = column(); + for (const name of knownChannels) { + let value = options[name]; if (value != null) { - const [V, setV] = column(value); - other[name] = V; - outputs.push((data, plan) => setV(maybeExpand(valueof(data, value), plan))); + if (value.definition) continue; // already planned + channels[name] = { + definition: value, + transform: (data) => maybeExpand(valueof(data, value), plan()) + }; } } - return [other, outputs]; + return [channels, setPlan]; } diff --git a/src/marks/image.js b/src/marks/image.js index 3eaef552d3..dea489736c 100644 --- a/src/marks/image.js +++ b/src/marks/image.js @@ -34,8 +34,13 @@ function isUrl(string) { // Disambiguates a constant src definition from a channel. A path or URL string // is assumed to be a constant; any other string is assumed to be a field name. -export function maybePathChannel(value) { - return typeof value === "string" && (isPath(value) || isUrl(value)) ? [undefined, value] : [value, undefined]; +function maybePathChannel(value) { + if (typeof value === "string" && (isPath(value) || isUrl(value))) return [undefined, value]; + if (value && value.definition) { + const [, f] = maybePathChannel(value.definition); + if (f !== undefined) return [undefined, f]; + } + return [value, undefined]; } export class Image extends Mark { diff --git a/src/marks/text.js b/src/marks/text.js index ccd5ec7fa7..d634f58b55 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -241,8 +241,12 @@ const fontSizes = new Set([ // - string : e.g., "12px" // - string : e.g., "80%" // Anything else is assumed to be a channel definition. -export function maybeFontSizeChannel(fontSize) { +function maybeFontSizeChannel(fontSize) { if (fontSize == null || typeof fontSize === "number") return [undefined, fontSize]; + if (fontSize && fontSize.definition) { + const [, f] = maybeFontSizeChannel(fontSize.definition); + if (f !== undefined) return [undefined, f]; + } if (typeof fontSize !== "string") return [fontSize, undefined]; fontSize = fontSize.trim().toLowerCase(); return fontSizes.has(fontSize) || /^[+-]?\d*\.?\d+(e[+-]?\d+)?(\w*|%)$/.test(fontSize) diff --git a/src/options.js b/src/options.js index 94ca533c1a..1d8437ef55 100644 --- a/src/options.js +++ b/src/options.js @@ -71,6 +71,7 @@ export function percentile(reduce) { // CSS color, use an accessor (d => d.red) instead. export function maybeColorChannel(value, defaultValue) { if (value === undefined) value = defaultValue; + if (value && value.definition && isColor(value.definition)) return [undefined, value.definition]; return value === null ? [undefined, "none"] : isColor(value) ? [undefined, value] : [value, undefined]; } @@ -78,6 +79,7 @@ export function maybeColorChannel(value, defaultValue) { // indicating a constant, and otherwise assumes that it’s a channel value. export function maybeNumberChannel(value, defaultValue) { if (value === undefined) value = defaultValue; + if (value && value.definition && typeof value.definition === "number") return [undefined, value.definition]; return value === null || typeof value === "number" ? [undefined, value] : [value, undefined]; } @@ -252,7 +254,15 @@ export function maybeColumn(source) { } export function labelof(value, defaultValue) { - return typeof value === "string" ? value : value && value.label !== undefined ? value.label : defaultValue; + return typeof value === "string" + ? value + : value + ? value.label !== undefined + ? value.label + : typeof value.definition === "string" + ? value.definition + : defaultValue + : defaultValue; } // Assuming that both x1 and x2 and lazy columns (per above), this derives a new diff --git a/src/symbols.js b/src/symbols.js index 8c02b900f9..9b3526b65c 100644 --- a/src/symbols.js +++ b/src/symbols.js @@ -55,6 +55,7 @@ export function maybeSymbol(symbol) { export function maybeSymbolChannel(symbol) { if (symbol == null || isSymbolObject(symbol)) return [undefined, symbol]; + if (symbol && symbol.definition && isSymbolObject(symbol.definition)) return [undefined, symbol.definition]; if (typeof symbol === "string") { const value = symbols.get(`${symbol}`.toLowerCase()); if (value) return [undefined, value]; diff --git a/src/transforms/map.js b/src/transforms/map.js index 1cf2bc21a0..9d0661e1ed 100644 --- a/src/transforms/map.js +++ b/src/transforms/map.js @@ -1,7 +1,7 @@ import {count, group, rank} from "d3"; import {maybeZ, take, valueof, maybeInput, column} from "../options.js"; import {basic} from "./basic.js"; -import {maybeExpand, facetReindex, maybeExpandOutputs} from "../facet.js"; +import {maybeExpand, facetReindex, maybeExpandChannels} from "../facet.js"; /** * ```js @@ -57,7 +57,7 @@ export function map(outputs = {}, options = {}) { const [output, setOutput] = column(input); return {key, input, output, setOutput, map: maybeMap(map)}; }); - const [other, facetOutputs] = maybeExpandOutputs(options); // TODO wrap outputs in facetReindex + const [other, setPlan] = maybeExpandChannels(options); return { ...basic(options, (data, facets) => { let plan; @@ -65,7 +65,7 @@ export function map(outputs = {}, options = {}) { const Z = maybeExpand(valueof(data, z), plan); const X = channels.map(({input}) => maybeExpand(valueof(data, input), plan)); const MX = channels.map(({setOutput}) => setOutput(new Array(plan ? plan.length : data.length))); - for (const o of facetOutputs) o(data, plan); // expand any extra channels + setPlan(plan); // expand any extra channels for (const facet of facets) { for (const I of Z ? group(facet, (i) => Z[i]).values() : [facet]) { channels.forEach(({map}, i) => map.map(I, X[i], MX[i])); diff --git a/src/transforms/stack.js b/src/transforms/stack.js index 1059d625c4..851ed4d0fa 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -1,6 +1,6 @@ import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3"; import {ascendingDefined} from "../defined.js"; -import {maybeExpand, facetReindex, maybeExpandOutputs} from "../facet.js"; +import {maybeExpand, facetReindex, maybeExpandChannels} from "../facet.js"; import {field, column, mid, range, valueof, one} from "../options.js"; import {maybeColumn, maybeZ, maybeZero} from "../options.js"; import {basic} from "./basic.js"; @@ -147,7 +147,7 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { const [Y2, setY2] = column(y); offset = maybeOffset(offset); order = maybeOrder(order, offset, ky); - const [other, outputs] = maybeExpandOutputs(options); // TODO wrap outputs in facetReindex + const [other, setPlan] = maybeExpandChannels(options); return [ basic(options, (data, facets) => { let plan; @@ -159,8 +159,8 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { const Y = maybeExpand(YS, plan); const Z = maybeExpand(ZS, plan); const O = order && maybeExpand(order(data, XS, YS, ZS), plan); + setPlan(plan); // expand any extra channels const n = plan ? plan.length : data.length; - for (const o of outputs) o(data, plan); // expand any extra channels const Y1 = setY1(new Float64Array(n)); const Y2 = setY2(new Float64Array(n)); const facetstacks = [];