Skip to content

year and integer type scales #1268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/axes.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function valueObject(channels, scales) {
Object.entries(channels).map(([name, {scale: scaleName, value}]) => {
let scale;
if (scaleName !== undefined) {
scale = scales[scaleName];
scale = scales[scaleName]?.apply;
}
return [name, scale === undefined ? value : map(value, scale)];
})
Expand Down
10 changes: 5 additions & 5 deletions src/facet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/legends/ramp.js
Original file line number Diff line number Diff line change
@@ -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 {Context, create} from "../context.js";
import {map} from "../options.js";
import {interpolatePiecewise} from "../scales/quantitative.js";
Expand Down
3 changes: 2 additions & 1 deletion src/legends/swatches.js
Original file line number Diff line number Diff line change
@@ -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 {Context, create} from "../context.js";
import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js";
import {isOrdinalScale, isThresholdScale} from "../scales.js";
Expand Down
122 changes: 82 additions & 40 deletions src/marks/axis.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {extent, format, utcFormat} from "d3";
import {extent, format, utcFormat, scaleLinear, scaleLog, scaleTime, scaleUtc} from "d3";
import {formatDefault} from "../format.js";
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, maybeInterval, 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";
Expand Down Expand Up @@ -138,7 +138,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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -500,34 +500,42 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
let channels;
const m = mark(
data,
initializer(options, function (data, facets, _channels, scales) {
const {[k]: scale} = scales;
initializer(options, function (data, facets, _channels, scales, dimensions) {
let {[k]: scale} = scales;
if (!scale) throw new Error(`missing scale: ${k}`);
if (scale.type === "identity" && (k === "x" || k === "y") && scale.range === undefined) {
const {width, height, marginLeft, marginTop, marginRight, marginBottom} = dimensions;
const range = k === "x" ? [marginLeft, width - marginRight] : [height - marginBottom, marginTop];
scale = {...scale, range, domain: range};
}
let {ticks, tickSpacing, interval} = options;
if (isTemporalScale(scale) && typeof ticks === "string") (interval = ticks), (ticks = undefined);
if (data == null) {
if (isIterable(ticks)) {
data = arrayify(ticks);
} else if (scale.ticks) {
if (ticks !== undefined) {
data = scale.ticks(ticks);
} else {
interval = maybeInterval(interval === undefined ? scale.interval : interval, scale.type);
if (interval !== undefined) {
// For time scales, we could pass the interval directly to
// 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());
data = interval.range(min, interval.offset(interval.floor(max))); // inclusive max
} else {
const tickFunction = inferTickFunction(scale);
if (tickFunction) {
if (ticks !== undefined) {
data = tickFunction(ticks);
} else {
const [min, max] = extent(scale.range());
ticks = (max - min) / (tickSpacing === undefined ? (k === "x" ? 80 : 35) : tickSpacing);
data = scale.ticks(ticks);
interval = maybeInterval(interval === undefined ? scale.interval : interval, scale.type);
if (interval !== undefined) {
// For time scales, we could pass the interval directly to
// 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);
data = interval.range(min, interval.offset(interval.floor(max))); // inclusive max
} else {
const [min, max] = extent(scale.range);
ticks = (max - min) / (tickSpacing === undefined ? (k === "x" ? 80 : 35) : tickSpacing);
data = tickFunction(ticks);
}
}
} else {
data = scale.domain;
}
} else {
data = scale.domain();
}
if (k === "y" || k === "x") {
facets = [range(data)];
Expand Down Expand Up @@ -562,15 +570,51 @@ function inferTextChannel(scale, ticks, tickFormat) {

// D3’s ordinal scales simply use toString by default, but if the ordinal scale
// 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.
// interval to the ordinal scale), we want Plot’s default formatter. As a
// heuristic to represent years on a linear scale without a comma, when the
// scale is integer and the domain covers [1500...2200] we default to "d".
function inferTickFormat(scale, ticks, tickFormat) {
return scale.tickFormat
? scale.tickFormat(isIterable(ticks) ? null : ticks, tickFormat)
: tickFormat === undefined
? formatDefault
: typeof tickFormat === "string"
? (isTemporal(scale.domain()) ? utcFormat : format)(tickFormat)
: constant(tickFormat);
switch (scale.type) {
case "point":
case "band":
return tickFormat === undefined
? isYearFormat(scale)
? format("d")
: formatDefault
: typeof tickFormat === "string"
? (isTemporal(scale.domain) ? utcFormat : format)(tickFormat)
: constant(tickFormat);
case "log":
return scaleLog()
.domain(scale.domain)
.tickFormat(isIterable(ticks) ? null : ticks, tickFormat);
case "time":
return scaleTime()
.domain(scale.domain)
.tickFormat(isIterable(ticks) ? null : ticks, tickFormat);
case "utc":
return scaleUtc()
.domain(scale.domain)
.tickFormat(isIterable(ticks) ? null : ticks, tickFormat);
default:
return isYearFormat(scale)
? format("d")
: scaleLinear()
.domain(scale.domain)
.tickFormat(isIterable(ticks) ? null : ticks, tickFormat);
}
}

function isYearFormat({integer, domain}) {
if (integer !== true) return;
for (const d of domain) if (typeof d !== "number" || d < 1500 || d > 2200) return;
return true;
}

function inferTickFunction({type, domain}) {
if (type === "point" || type === "band") return;
const S = type === "log" ? scaleLog : type === "time" ? scaleTime : type === "utc" ? scaleUtc : scaleLinear;
return S().domain(domain).ticks;
}

const shapeTickBottom = {
Expand Down Expand Up @@ -601,24 +645,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;
if (scale.bandwidth !== undefined || !label?.inferred) return label;
const order = inferScaleOrder(scale);
return order
? key === "x" || labelAnchor === "center"
Expand All @@ -628,3 +666,7 @@ function inferAxisLabel(key, scale, labelAnchor) {
: `${order < 0 ? "↑ " : "↓ "}${label}`
: label;
}

export function inferFontVariant(scale) {
return isOrdinalScale(scale) && scale.interval === undefined ? undefined : "tabular-nums";
}
4 changes: 2 additions & 2 deletions src/marks/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/marks/geo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/marks/raster.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class Raster extends AbstractRaster {
return super.scale(channels, scales, context);
}
render(index, scales, channels, dimensions, context) {
const color = scales.color ?? ((x) => x);
const color = scales.color?.apply ?? ((x) => x);
const {x: X, y: Y} = channels;
const {document} = context;
const [x1, y1, x2, y2] = renderBounds(channels, dimensions, context);
Expand Down
4 changes: 2 additions & 2 deletions src/marks/tick.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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];
Expand Down
18 changes: 9 additions & 9 deletions src/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Mark} from "./mark.js";
import {axisFx, axisFy, axisX, axisY, gridFx, gridFy, gridX, gridY} from "./marks/axis.js";
import {frame} from "./marks/frame.js";
import {arrayify, isColor, isIterable, isNone, isScaleOptions, map, yes, maybeInterval} from "./options.js";
import {Scales, ScaleFunctions, autoScaleRange, exposeScales, innerDimensions, outerDimensions} from "./scales.js";
import {Scales, instantiateScales, autoScaleRange, exposeScales, innerDimensions, outerDimensions} from "./scales.js";
import {position, registry as scaleRegistry} from "./scales/index.js";
import {applyInlineStyles, maybeClassName} from "./style.js";
import {consumeWarnings, warn} from "./warnings.js";
Expand Down Expand Up @@ -133,11 +133,11 @@ export function plot(options = {}) {

// Initalize the scales and dimensions.
const scaleDescriptors = Scales(addScaleChannels(channelsByScale, stateByMark), options);
const scales = ScaleFunctions(scaleDescriptors);
const dimensions = Dimensions(scaleDescriptors, marks, options);

autoScaleRange(scaleDescriptors, dimensions);

const scales = instantiateScales(scaleDescriptors);
const {fx, fy} = scales;
const subdimensions = fx || fy ? innerDimensions(scaleDescriptors, dimensions) : dimensions;
const superdimensions = fx || fy ? actualDimensions(scales, dimensions) : dimensions;
Expand Down Expand Up @@ -192,7 +192,7 @@ export function plot(options = {}) {
addScaleChannels(newChannelsByScale, stateByMark, (key) => newByScale.has(key));
addScaleChannels(channelsByScale, stateByMark, (key) => newByScale.has(key));
const newScaleDescriptors = inheritScaleLabels(Scales(newChannelsByScale, options), scaleDescriptors);
const newScales = ScaleFunctions(newScaleDescriptors);
const newScales = instantiateScales(newScaleDescriptors);
Object.assign(scaleDescriptors, newScaleDescriptors);
Object.assign(scales, newScales);
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -301,7 +301,7 @@ export function plot(options = {}) {
}
}

figure.scale = exposeScales(scaleDescriptors);
figure.scale = exposeScales(scales);
figure.legend = exposeLegends(scaleDescriptors, context, options);

const w = consumeWarnings();
Expand Down Expand Up @@ -656,9 +656,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];
}
Loading