diff --git a/src/options.js b/src/options.js index e7c6685d6d..1bd96f1780 100644 --- a/src/options.js +++ b/src/options.js @@ -12,6 +12,7 @@ export function valueof(data, value, arrayType) { : type === "function" ? map(data, value, arrayType) : type === "number" || value instanceof Date || type === "boolean" ? map(data, constant(value), arrayType) : value && typeof value.transform === "function" ? arrayify(value.transform(data), arrayType) + : value && value.alias !== undefined ? value // TODO cleaner? : arrayify(value, arrayType); // preserve undefined type } diff --git a/src/plot.js b/src/plot.js index 5c7fa2c0ed..06f9048901 100644 --- a/src/plot.js +++ b/src/plot.js @@ -76,7 +76,7 @@ export function plot(options = {}) { : mark.facet === "exclude" ? facetsExclude || (facetsExclude = facetsIndex.map(f => Uint32Array.from(difference(facetIndex, f)))) : undefined; const {data, facets, channels} = mark.initialize(markFacets, facetChannels); - applyScaleTransforms(channels, options); + applyScaleTransforms(channels, stateByMark, options); stateByMark.set(mark, {data, facets, channels}); } @@ -102,7 +102,7 @@ export function plot(options = {}) { if (facets !== undefined) state.facets = facets; if (channels !== undefined) { inferChannelScale(channels, mark); - applyScaleTransforms(channels, options); + applyScaleTransforms(channels, stateByMark, options); Object.assign(state.channels, channels); for (const {scale} of Object.values(channels)) if (scale != null) newByScale.add(scale); } @@ -251,6 +251,7 @@ export class Mark { const {facet = "auto", sort, dx, dy, clip, channels: extraChannels} = options; const names = new Set(); this.data = data; + this.options = options; this.sort = isDomainSort(sort) ? sort : null; this.initializer = initializer(options).initializer; this.transform = this.initializer ? options.transform : basic(options).transform; @@ -295,6 +296,19 @@ export class Mark { plot({marks = [], ...options} = {}) { return plot({...options, marks: [...marks, this]}); } + with(Mark, options) { + const m = [ + this, + Mark(this.data, { + ...this.options, + ...Object.fromEntries(this.channels.map(({name}) => [name, ({alias: (stateByMark) => stateByMark.get(this).channels[name].value})])), + ...options + }) + ]; + m.with = () => m; // TODO chained with + m.plot = Mark.prototype.plot; + return m; + } } export function marks(...marks) { @@ -317,11 +331,13 @@ class Render extends Mark { } // Note: mutates channel.value to apply the scale transform, if any. -function applyScaleTransforms(channels, options) { +function applyScaleTransforms(channels, stateByMark, options) { for (const name in channels) { const channel = channels[name]; - const {scale} = channel; - if (scale != null) { + const {value, scale} = channel; + if (value && value.alias !== undefined) { + channel.value = value.alias(stateByMark); + } else if (scale != null) { const {percent, transform = percent ? x => x * 100 : undefined} = options[scale] || {}; if (transform != null) channel.value = map(channel.value, transform); } diff --git a/test/output/randomWith.svg b/test/output/randomWith.svg new file mode 100644 index 0000000000..eee4a59b07 --- /dev/null +++ b/test/output/randomWith.svg @@ -0,0 +1,600 @@ + + + + + −8 + + + −6 + + + −4 + + + −2 + + + 0 + + + 2 + + + 4 + + + 6 + + + 8 + + + 10 + + + 12 + + + 14 + + + 16 + + + 18 + + + 20 + + + + + 0 + + + 50 + + + 100 + + + 150 + + + 200 + + + 250 + + + 300 + + + 350 + + + 400 + + + 450 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/covid-ihme-projected-deaths.js b/test/plots/covid-ihme-projected-deaths.js index 560ab55b46..2cb3a29aa2 100644 --- a/test/plots/covid-ihme-projected-deaths.js +++ b/test/plots/covid-ihme-projected-deaths.js @@ -36,10 +36,7 @@ export default async function() { x: "date", y: "mean", fill: "currentColor" - }), - Plot.text([data[i]], { - x: "date", - y: "mean", + }).with(Plot.text, { text: "mean", textAnchor: "start", dx: 6 diff --git a/test/plots/index.js b/test/plots/index.js index bfcf4724a3..63b5b9a3c1 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -147,6 +147,7 @@ export {default as randomBins} from "./random-bins.js"; export {default as randomBinsXY} from "./random-bins-xy.js"; export {default as randomQuantile} from "./random-quantile.js"; export {default as randomWalk} from "./random-walk.js"; +export {default as randomWith} from "./random-with.js"; export {default as rectBand} from "./rect-band.js"; export {default as seattlePrecipitationRule} from "./seattle-precipitation-rule.js"; export {default as seattlePrecipitationSum} from "./seattle-precipitation-sum.js"; diff --git a/test/plots/random-with.js b/test/plots/random-with.js new file mode 100644 index 0000000000..50b497fd21 --- /dev/null +++ b/test/plots/random-with.js @@ -0,0 +1,11 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const randomNormal = d3.randomNormal.source(d3.randomLcg(42))(); + return Plot.plot({ + marks: [ + Plot.lineY({length: 500}, Plot.mapY("cumsum", {y: randomNormal, stroke: "blue"})).with(Plot.dot) + ] + }); +}