diff --git a/src/axes.js b/src/axes.js index 8ff27f4084..14779c6b3e 100644 --- a/src/axes.js +++ b/src/axes.js @@ -1,11 +1,6 @@ import {format, utcFormat} from "d3"; import {formatIsoDate} from "./format.js"; import {constant, isTemporal, string} from "./options.js"; -import {isOrdinalScale} from "./scales.js"; - -export function inferFontVariant(scale) { - return isOrdinalScale(scale) && scale.interval === undefined ? undefined : "tabular-nums"; -} // D3 doesn’t provide a tick format for ordinal scales; we want shorthand when // an ordinal domain is numbers or dates, and we want null to mean the empty diff --git a/src/channel.js b/src/channel.js index bb67310dd7..c1fec6b219 100644 --- a/src/channel.js +++ b/src/channel.js @@ -27,7 +27,7 @@ export function createChannels(channels, data) { export function valueObject(channels, scales) { const values = Object.fromEntries( Object.entries(channels).map(([name, {scale: scaleName, value}]) => { - const scale = scaleName == null ? null : scales[scaleName]; + const scale = scaleName == null ? null : scales[scaleName]?.apply; return [name, scale == null ? value : map(value, scale)]; }) ); diff --git a/src/facet.js b/src/facet.js index 3a6daed12c..2cdcf3b21d 100644 --- a/src/facet.js +++ b/src/facet.js @@ -55,11 +55,11 @@ export function facetGroups(data, {fx, fy}) { } export function facetTranslate(fx, fy, {marginTop, marginLeft}) { - return fx && fy - ? ({x, y}) => `translate(${fx(x) - marginLeft},${fy(y) - marginTop})` - : fx - ? ({x}) => `translate(${fx(x) - marginLeft},0)` - : ({y}) => `translate(0,${fy(y) - marginTop})`; + return fx?.apply && fy?.apply + ? ({x, y}) => `translate(${fx.apply(x) - marginLeft},${fy.apply(y) - marginTop})` + : fx?.apply + ? ({x}) => `translate(${fx.apply(x) - marginLeft},0)` + : ({y}) => `translate(0,${fy.apply(y) - marginTop})`; } // Returns an index that for each facet lists all the elements present in other diff --git a/src/legends/ramp.js b/src/legends/ramp.js index d52f2d8ccd..7119061e39 100644 --- a/src/legends/ramp.js +++ b/src/legends/ramp.js @@ -1,5 +1,5 @@ import {quantize, interpolateNumber, piecewise, format, scaleBand, scaleLinear, axisBottom} from "d3"; -import {inferFontVariant} from "../axes.js"; +import {inferFontVariant} from "../marks/axis.js"; import {createContext, create} from "../context.js"; import {map} from "../options.js"; import {interpolatePiecewise} from "../scales/quantitative.js"; diff --git a/src/legends/swatches.js b/src/legends/swatches.js index 8537a3842e..2538609da0 100644 --- a/src/legends/swatches.js +++ b/src/legends/swatches.js @@ -1,5 +1,6 @@ import {pathRound as path} from "d3"; -import {inferFontVariant, maybeAutoTickFormat} from "../axes.js"; +import {inferFontVariant} from "../marks/axis.js"; +import {maybeAutoTickFormat} from "../axes.js"; import {createContext, create} from "../context.js"; import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js"; import {isOrdinalScale, isThresholdScale} from "../scales.js"; diff --git a/src/mark.d.ts b/src/mark.d.ts index d8a2bc9538..7296dd55f0 100644 --- a/src/mark.d.ts +++ b/src/mark.d.ts @@ -2,7 +2,7 @@ import type {ChannelDomainSort, Channels, ChannelValue, ChannelValues, ChannelVa import type {Context} from "./context.js"; import type {Dimensions} from "./dimensions.js"; import type {plot} from "./plot.js"; -import type {ScaleFunctions} from "./scales.js"; +import type {InstanciatedScales} from "./scales.js"; import type {InitializerFunction, SortOrder, TransformFunction} from "./transforms/basic.js"; /** @@ -40,8 +40,8 @@ export type Data = Iterable | ArrayLike; export type RenderFunction = ( /** The mark’s (filtered and transformed) index. */ index: number[], - /** The plot’s scale functions. */ - scales: ScaleFunctions, + /** The plot’s instanciated scales. */ + scales: InstanciatedScales, /** The mark’s (possibly scaled and transformed) channel values. */ values: ChannelValues, /** The plot’s dimensions. */ @@ -459,5 +459,5 @@ export class RenderableMark extends Mark { /** A compound Mark, comprising other marks. */ export type CompoundMark = Markish[] & Pick; -/** Given an array of marks, returns a compound mark; supports *mark.plot shorthand. */ +/** Given an array of marks, returns a compound mark; supports *mark*.plot shorthand. */ export function marks(...marks: Markish[]): CompoundMark; diff --git a/src/marks/axis.js b/src/marks/axis.js index 7ebb0307be..ff2978e4cc 100644 --- a/src/marks/axis.js +++ b/src/marks/axis.js @@ -4,7 +4,7 @@ import {marks} from "../mark.js"; import {radians} from "../math.js"; import {range, valueof, arrayify, constant, keyword, identity, number} from "../options.js"; import {isNoneish, isIterable, isTemporal, maybeRangeInterval, orderof} from "../options.js"; -import {isTemporalScale} from "../scales.js"; +import {isOrdinalScale, isTemporalScale} from "../scales.js"; import {offset} from "../style.js"; import {initializer} from "../transforms/basic.js"; import {ruleX, ruleY} from "./rule.js"; @@ -136,7 +136,7 @@ function axisKy( initializer: function (data, facets, channels, scales, dimensions) { const scale = scales[k]; const {marginTop, marginRight, marginBottom, marginLeft} = (k === "y" && dimensions.inset) || dimensions; - const cla = labelAnchor ?? (scale.bandwidth ? "center" : "top"); + const cla = labelAnchor ?? (scale.bandwidth === undefined ? "top" : "center"); const clo = labelOffset ?? (anchor === "right" ? marginRight : marginLeft) - 3; if (cla === "center") { this.textAnchor = undefined; // middle @@ -245,7 +245,7 @@ function axisKx( initializer: function (data, facets, channels, scales, dimensions) { const scale = scales[k]; const {marginTop, marginRight, marginBottom, marginLeft} = (k === "x" && dimensions.inset) || dimensions; - const cla = labelAnchor ?? (scale.bandwidth ? "center" : "right"); + const cla = labelAnchor ?? (scale.bandwidth === undefined ? "right" : "center"); const clo = labelOffset ?? (anchor === "top" ? marginTop : marginBottom) - 3; if (cla === "center") { this.frameAnchor = anchor; @@ -507,9 +507,9 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) { if (data == null) { if (isIterable(ticks)) { data = arrayify(ticks); - } else if (scale.ticks) { + } else if (scale._tickFunction) { if (ticks !== undefined) { - data = scale.ticks(ticks); + data = scale._tickFunction(ticks); } else { interval = maybeRangeInterval(interval === undefined ? scale.interval : interval, scale.type); if (interval !== undefined) { @@ -517,16 +517,16 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) { // scale.ticks because it’s supported by d3.utcTicks; but // quantitative scales and d3.ticks do not support numeric // intervals for scale.ticks, so we compute them here. - const [min, max] = extent(scale.domain()); + const [min, max] = extent(scale.domain); data = interval.range(min, interval.offset(interval.floor(max))); // inclusive max } else { - const [min, max] = extent(scale.range()); + const [min, max] = extent(scale.range); ticks = (max - min) / (tickSpacing === undefined ? (k === "x" ? 80 : 35) : tickSpacing); - data = scale.ticks(ticks); + data = scale._tickFunction(ticks); } } } else { - data = scale.domain(); + data = scale.domain; } if (k === "y" || k === "x") { facets = [range(data)]; @@ -563,12 +563,12 @@ function inferTextChannel(scale, ticks, tickFormat) { // domain (or ticks) are numbers or dates (say because we’re applying a time // interval to the ordinal scale), we want Plot’s default formatter. function inferTickFormat(scale, ticks, tickFormat) { - return scale.tickFormat - ? scale.tickFormat(isIterable(ticks) ? null : ticks, tickFormat) + return scale._tickFormat + ? scale._tickFormat(isIterable(ticks) ? null : ticks, tickFormat) : tickFormat === undefined ? formatDefault : typeof tickFormat === "string" - ? (isTemporal(scale.domain()) ? utcFormat : format)(tickFormat) + ? (isTemporal(scale.domain) ? utcFormat : format)(tickFormat) : constant(tickFormat); } @@ -600,24 +600,18 @@ const shapeTickRight = { } }; -// TODO Unify this with the other inferFontVariant; here we only have a scale -// function rather than a scale descriptor. -function inferFontVariant(scale) { - return scale.bandwidth && scale.interval === undefined ? undefined : "tabular-nums"; -} - // Determines whether the scale points in the “positive” (right or down) or // “negative” (left or up) direction; if the scale order cannot be determined, // returns NaN; used to assign an appropriate label arrow. function inferScaleOrder(scale) { - return Math.sign(orderof(scale.domain())) * Math.sign(orderof(scale.range())); + return Math.sign(orderof(scale.domain)) * Math.sign(orderof(scale.range)); } // Takes the scale label, and if this is not an ordinal scale and the label was // inferred from an associated channel, adds an orientation-appropriate arrow. function inferAxisLabel(key, scale, labelAnchor) { - const label = scale.label; - if (scale.bandwidth || !label?.inferred) return label; + const label = scale._label; + if (scale.bandwidth !== undefined || !label?.inferred) return label; const order = inferScaleOrder(scale); return order ? key === "x" || labelAnchor === "center" @@ -627,3 +621,7 @@ function inferAxisLabel(key, scale, labelAnchor) { : `${order < 0 ? "↑ " : "↓ "}${label}` : label; } + +export function inferFontVariant(scale) { + return isOrdinalScale(scale) && scale.interval === undefined ? undefined : "tabular-nums"; +} diff --git a/src/marks/bar.js b/src/marks/bar.js index d7afbb37c4..95180687b4 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -57,12 +57,12 @@ export class AbstractBar extends Mark { } _width({x}, {x: X}, {marginRight, marginLeft, width}) { const {insetLeft, insetRight} = this; - const bandwidth = X && x ? x.bandwidth() : width - marginRight - marginLeft; + const bandwidth = X && x ? x.bandwidth : width - marginRight - marginLeft; return Math.max(0, bandwidth - insetLeft - insetRight); } _height({y}, {y: Y}, {marginTop, marginBottom, height}) { const {insetTop, insetBottom} = this; - const bandwidth = Y && y ? y.bandwidth() : height - marginTop - marginBottom; + const bandwidth = Y && y ? y.bandwidth : height - marginTop - marginBottom; return Math.max(0, bandwidth - insetTop - insetBottom); } } diff --git a/src/marks/geo.js b/src/marks/geo.js index 7797189fee..b87a58a3c7 100644 --- a/src/marks/geo.js +++ b/src/marks/geo.js @@ -60,7 +60,7 @@ function scaleProjection({x: X, y: Y}) { Y ??= (y) => y; return geoTransform({ point(x, y) { - this.stream.point(X(x), Y(y)); + this.stream.point(X.apply(x), Y.apply(y)); } }); } diff --git a/src/marks/raster.js b/src/marks/raster.js index fd601e3ced..813f243011 100644 --- a/src/marks/raster.js +++ b/src/marks/raster.js @@ -98,7 +98,7 @@ export class Raster extends AbstractRaster { return super.scale(channels, scales, context); } render(index, scales, values, dimensions, context) { - const color = scales[values.channels.fill?.scale] ?? ((x) => x); + const color = scales[values.channels.fill?.scale]?.apply ?? ((x) => x); const {x: X, y: Y} = values; const {document} = context; const [x1, y1, x2, y2] = renderBounds(values, dimensions, context); diff --git a/src/marks/tick.js b/src/marks/tick.js index 6901471f0a..e5aef12dbc 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -63,7 +63,7 @@ export class TickX extends AbstractTick { } _y2({y}, {y: Y}, {height, marginBottom}) { const {insetBottom} = this; - return Y && y ? (i) => Y[i] + y.bandwidth() - insetBottom : height - marginBottom - insetBottom; + return Y && y ? (i) => Y[i] + y.bandwidth - insetBottom : height - marginBottom - insetBottom; } } @@ -90,7 +90,7 @@ export class TickY extends AbstractTick { } _x2({x}, {x: X}, {width, marginRight}) { const {insetRight} = this; - return X && x ? (i) => X[i] + x.bandwidth() - insetRight : width - marginRight - insetRight; + return X && x ? (i) => X[i] + x.bandwidth - insetRight : width - marginRight - insetRight; } _y1(scales, {y: Y}) { return (i) => Y[i]; diff --git a/src/plot.js b/src/plot.js index 76c99a55d8..94c10d57d9 100644 --- a/src/plot.js +++ b/src/plot.js @@ -133,11 +133,11 @@ export function plot(options = {}) { // Initalize the scales and dimensions. const scaleDescriptors = createScales(addScaleChannels(channelsByScale, stateByMark), options); - const scales = createScaleFunctions(scaleDescriptors); const dimensions = createDimensions(scaleDescriptors, marks, options); autoScaleRange(scaleDescriptors, dimensions); + const scales = createScaleFunctions(scaleDescriptors); const {fx, fy} = scales; const subdimensions = fx || fy ? innerDimensions(scaleDescriptors, dimensions) : dimensions; const superdimensions = fx || fy ? actualDimensions(scales, dimensions) : dimensions; @@ -235,7 +235,7 @@ export function plot(options = {}) { // Render facets. if (facets !== undefined) { - const facetDomains = {x: fx?.domain(), y: fy?.domain()}; + const facetDomains = {x: fx?.domain, y: fy?.domain}; // Sort the facets to match the fx and fy domains; this is needed because // the facets were constructed prior to the fx and fy scales. @@ -638,9 +638,9 @@ function actualDimensions({fx, fy}, dimensions) { } function outerRange(scale) { - const domain = scale.domain(); - let x1 = scale(domain[0]); - let x2 = scale(domain[domain.length - 1]); + const {domain} = scale; + let x1 = scale.apply(domain[0]); + let x2 = scale.apply(domain[domain.length - 1]); if (x2 < x1) [x1, x2] = [x2, x1]; - return [x1, x2 + scale.bandwidth()]; + return [x1, x2 + scale.bandwidth]; } diff --git a/src/scales.d.ts b/src/scales.d.ts index e46e527e0f..1994ddd2a2 100644 --- a/src/scales.d.ts +++ b/src/scales.d.ts @@ -160,10 +160,9 @@ export type ColorScheme = ColorSchemeCase | (Lowercase & Record export type ScaleName = "x" | "y" | "fx" | "fy" | "r" | "color" | "opacity" | "symbol" | "length"; /** - * The instantiated scales’ apply functions; passed to marks and initializers - * for rendering. + * The instantiated scales; passed to marks and initializers for rendering. */ -export type ScaleFunctions = {[key in ScaleName]?: (value: any) => any}; +export type InstanciatedScales = {[key in ScaleName]?: Scale}; /** * The supported scale types. For quantitative data, one of: diff --git a/src/scales.js b/src/scales.js index 1e0138380c..980255fcf1 100644 --- a/src/scales.js +++ b/src/scales.js @@ -101,12 +101,17 @@ export function createScaleFunctions(scales) { return Object.fromEntries( Object.entries(scales) .filter(([, {scale}]) => scale) // drop identity scales - .map(([name, {scale, type, interval, label}]) => { - scale.type = type; // for axis - if (interval != null) scale.interval = interval; // for axis - if (label != null) scale.label = label; // for axis - return [name, scale]; - }) + .map(([name, scale]) => [ + name, + { + ...exposeScale(scale), + // for axis + _label: scale.label, + _tickFormat: scale.scale.tickFormat, + _tickFunction: scale.scale.ticks, + ...(scale.type === "identity" && {range: slice(scale.range)}) + } + ]) ); } @@ -472,10 +477,10 @@ export function isDivergingScale({type}) { // dimension (whereas a dot will simply be drawn in the center). export function isCollapsed(scale) { if (scale === undefined) return true; // treat missing scale as collapsed - const domain = scale.domain(); - const value = scale(domain[0]); + const {domain} = scale; + const value = scale.apply(domain[0]); for (let i = 1, n = domain.length; i < n; ++i) { - if (scale(domain[i]) - value) { + if (scale.apply(domain[i]) - value) { return false; } } diff --git a/src/style.js b/src/style.js index e247afece4..a6e2342704 100644 --- a/src/style.js +++ b/src/style.js @@ -401,8 +401,8 @@ export function applyStyle(selection, name, value) { export function applyTransform(selection, mark, {x, y}, tx = offset, ty = offset) { tx += mark.dx; ty += mark.dy; - if (x?.bandwidth) tx += x.bandwidth() / 2; - if (y?.bandwidth) ty += y.bandwidth() / 2; + if (x?.bandwidth !== undefined) tx += x.bandwidth / 2; + if (y?.bandwidth !== undefined) ty += y.bandwidth / 2; if (tx || ty) selection.attr("transform", `translate(${tx},${ty})`); } diff --git a/src/transforms/basic.d.ts b/src/transforms/basic.d.ts index 204acf39bc..1d988b6d64 100644 --- a/src/transforms/basic.d.ts +++ b/src/transforms/basic.d.ts @@ -1,7 +1,7 @@ import type {ChannelName, Channels, ChannelValue} from "../channel.js"; import type {Context} from "../context.js"; import type {Dimensions} from "../dimensions.js"; -import type {ScaleFunctions} from "../scales.js"; +import type {InstanciatedScales} from "../scales.js"; /** * A mark transform function is passed the mark’s *data* and a nested index into @@ -40,7 +40,7 @@ export type InitializerFunction = ( data: any[], facets: number[][], channels: Channels, - scales: ScaleFunctions, + scales: InstanciatedScales, dimensions: Dimensions, context: Context ) => { diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js index 2cf0314447..3f4efe13b3 100644 --- a/src/transforms/dodge.js +++ b/src/transforms/dodge.js @@ -70,7 +70,7 @@ function dodge(y, x, anchor, padding, options) { if (!channels[x]) throw new Error(`missing channel: ${x}`); ({[x]: X} = applyPosition(channels, scales, context)); const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3; - if (R) R = valueof(R.value, scales[R.scale] || identity, Float64Array); + if (R) R = valueof(R.value, scales[R.scale]?.apply || identity, Float64Array); let [ky, ty] = anchor(dimensions); const compare = ky ? compareAscending : compareSymmetric; const Y = new Float64Array(X.length); diff --git a/test/transforms/remap.js b/test/transforms/remap.js index 2b1214d869..5f3a68a029 100644 --- a/test/transforms/remap.js +++ b/test/transforms/remap.js @@ -11,7 +11,7 @@ export function remap(outputs = {}, options) { if (!channel) throw new Error(`missing channel: ${name}`); const V = Array.from(channel.value); const n = V.length; - const scale = scales[channel.scale]; + const scale = scales[channel.scale]?.apply; if (scale) for (let i = 0; i < n; ++i) V[i] = scale(V[i]); for (let i = 0; i < n; ++i) V[i] = map(V[i], i, V); return [name, {value: V}];