diff --git a/src/marks/axis.js b/src/marks/axis.js
index 44d8190277..11984ca935 100644
--- a/src/marks/axis.js
+++ b/src/marks/axis.js
@@ -507,54 +507,56 @@ function labelOptions(
function axisMark(mark, k, ariaLabel, data, options, initialize) {
let channels;
- const m = mark(
- data,
- initializer(options, function (data, facets, _channels, scales, dimensions, context) {
- const initializeFacets = data == null && (k === "fx" || k === "fy");
- const {[k]: scale} = scales;
- if (!scale) throw new Error(`missing scale: ${k}`);
- 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);
+
+ function axisInitializer(data, facets, _channels, scales, dimensions, context) {
+ const initializeFacets = data == null && (k === "fx" || k === "fy");
+ const {[k]: scale} = scales;
+ if (!scale) throw new Error(`missing scale: ${k}`);
+ 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 = maybeRangeInterval(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 {
- interval = maybeRangeInterval(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 = scale.ticks(ticks);
- }
+ const [min, max] = extent(scale.range());
+ ticks = (max - min) / (tickSpacing === undefined ? (k === "x" ? 80 : 35) : tickSpacing);
+ data = scale.ticks(ticks);
}
- } else {
- data = scale.domain();
- }
- if (k === "y" || k === "x") {
- facets = [range(data)];
- } else {
- channels[k] = {scale: k, value: identity};
}
+ } else {
+ data = scale.domain();
}
- initialize?.call(this, scale, data, ticks, channels);
- const initializedChannels = Object.fromEntries(
- Object.entries(channels).map(([name, channel]) => {
- return [name, {...channel, value: valueof(data, channel.value)}];
- })
- );
- if (initializeFacets) facets = context.filterFacets(data, initializedChannels);
- return {data, facets, channels: initializedChannels};
- })
- );
+ if (k === "y" || k === "x") {
+ facets = [range(data)];
+ } else {
+ channels[k] = {scale: k, value: identity};
+ }
+ }
+ initialize?.call(this, scale, data, ticks, channels);
+ const initializedChannels = Object.fromEntries(
+ Object.entries(channels).map(([name, channel]) => {
+ return [name, {...channel, value: valueof(data, channel.value)}];
+ })
+ );
+ if (initializeFacets) facets = context.filterFacets(data, initializedChannels);
+ return {data, facets, channels: initializedChannels};
+ }
+
+ // Apply any basic initializers after the axis initializer computes the ticks.
+ const basicInitializer = initializer(options).initializer;
+ const m = mark(data, initializer({...options, initializer: axisInitializer}, basicInitializer));
if (data == null) {
channels = m.channels;
m.channels = {};
diff --git a/test/output/axisFilter.svg b/test/output/axisFilter.svg
new file mode 100644
index 0000000000..8218746e64
--- /dev/null
+++ b/test/output/axisFilter.svg
@@ -0,0 +1,45 @@
+
\ No newline at end of file
diff --git a/test/output/trafficHorizon.html b/test/output/trafficHorizon.html
index dc2d132b0f..a05e049014 100644
--- a/test/output/trafficHorizon.html
+++ b/test/output/trafficHorizon.html
@@ -50,1682 +50,1680 @@
white-space: pre;
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Jan 412 AM
- 12 PM
- Jan 512 AM
- 12 PM
- Jan 612 AM
- 12 PM
- Jan 712 AM
- 12 PM
- Jan 812 AM
- 12 PM
- Jan 912 AM
- 12 PM
-
-
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- Von der Heydt
+ Von der Heydt
- Kirschheck
+ Kirschheck
- Saarbrücken-Neuhaus
+ Saarbrücken-Neuhaus
- Riegelsberg
+ Riegelsberg
- Holz
+ Holz
- Göttelborn
+ Göttelborn
- Illingen
+ Illingen
- AS Eppelborn
+ AS Eppelborn
- Hasborn
+ Hasborn
- Kastel
+ Kastel
- Otzenhausen
+ Otzenhausen
- Bierfeld
+ Bierfeld
- Nonnweiler
+ Nonnweiler
- Hetzerath
+ Hetzerath
- Laufeld
+ Laufeld
- Nettersheim
+ Nettersheim
- Euskirchen/Bliesheim
+ Euskirchen/Bliesheim
- Hürth
+ Hürth
- Köln-Nord
+ Köln-Nord
- Schloss Burg
+ Schloss Burg
- Hagen-Vorhalle
+ Hagen-Vorhalle
- Hengsen
+ Hengsen
- Unna
+ Unna
- Ascheberg
+ Ascheberg
- Ladbergen
+ Ladbergen
- Lotte
+ Lotte
- HB-Silbersee
+ HB-Silbersee
- HB-Weserbrücke
+ HB-Weserbrücke
- HB-Mahndorfer See
+ HB-Mahndorfer See
- Groß Ippener
+ Groß Ippener
- Uphusen
+ Uphusen
- Bockel
+ Bockel
- Dibbersen
+ Dibbersen
- Glüsingen
+ Glüsingen
- Barsbüttel
+ Barsbüttel
- Bad Schwartau
+ Bad Schwartau
- Oldenburg (Holstein)
+ Oldenburg (Holstein)
- Neustadt i. H.-Süd
+ Neustadt i. H.-Süd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12 PM
+ Jan 512 AM
+ 12 PM
+ Jan 612 AM
+ 12 PM
+ Jan 712 AM
+ 12 PM
+ Jan 812 AM
+ 12 PM
+ Jan 912 AM
+ 12 PM
diff --git a/test/output/usStatePopulationChangeRelative.svg b/test/output/usStatePopulationChangeRelative.svg
new file mode 100644
index 0000000000..887374e72a
--- /dev/null
+++ b/test/output/usStatePopulationChangeRelative.svg
@@ -0,0 +1,220 @@
+
\ No newline at end of file
diff --git a/test/plots/axis-filter.ts b/test/plots/axis-filter.ts
new file mode 100644
index 0000000000..41f9475d5f
--- /dev/null
+++ b/test/plots/axis-filter.ts
@@ -0,0 +1,18 @@
+import * as Plot from "@observablehq/plot";
+
+export async function axisFilter() {
+ return Plot.plot({
+ height: 100,
+ marks: [
+ Plot.dot([
+ ["A", 0],
+ ["B", 2],
+ [0, 1]
+ ]),
+ Plot.gridX({filter: (d) => d}),
+ Plot.gridY({filter: (d) => d}),
+ Plot.axisX({filter: (d) => d}),
+ Plot.axisY({filter: (d) => d})
+ ]
+ });
+}
diff --git a/test/plots/index.ts b/test/plots/index.ts
index bba13661b0..c81803dd82 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -27,6 +27,7 @@ export * from "./athletes-weight-cumulative.js";
export * from "./athletes-weight.js";
export * from "./autoplot.js";
export * from "./availability.js";
+export * from "./axis-filter.js";
export * from "./axis-labels.js";
export * from "./ballot-status-race.js";
export * from "./band-clip.js";
diff --git a/test/plots/traffic-horizon.ts b/test/plots/traffic-horizon.ts
index 3b229abad9..14ee18d1c1 100644
--- a/test/plots/traffic-horizon.ts
+++ b/test/plots/traffic-horizon.ts
@@ -10,9 +10,8 @@ export async function trafficHorizon() {
return Plot.plot({
width: 960,
height: 1100,
- x: {
- axis: "top"
- },
+ margin: 0,
+ marginTop: 30,
y: {
axis: null,
domain: [0, step]
@@ -27,16 +26,12 @@ export async function trafficHorizon() {
legend: true
},
fy: {
- axis: null,
domain: data.map((d) => d.location) // respect input order
},
- facet: {
- data,
- y: "location"
- },
marks: [
- ticks.map((t) => Plot.areaY(data, {x: "date", y: (d) => d.vehicles - t, fill: t, clip: true})),
- Plot.text(data, Plot.selectFirst({text: "location", frameAnchor: "left"}))
+ ticks.map((t) => Plot.areaY(data, {x: "date", y: (d) => d.vehicles - t, fy: "location", fill: t, clip: true})),
+ Plot.axisFy({frameAnchor: "left", label: null}),
+ Plot.axisX({anchor: "top", filter: (d, i) => i > 0}) // drop first tick
]
});
}
diff --git a/test/plots/us-state-population-change.ts b/test/plots/us-state-population-change.ts
index dad80d4c0d..ed87f456e9 100644
--- a/test/plots/us-state-population-change.ts
+++ b/test/plots/us-state-population-change.ts
@@ -33,3 +33,36 @@ export async function usStatePopulationChange() {
]
});
}
+
+export async function usStatePopulationChangeRelative() {
+ const statepop = await d3.csv("data/us-state-population-2010-2019.csv", d3.autoType);
+ const change = new Map(statepop.map((d) => [d.State, (d[2019] - d[2010]) / d[2010]]));
+ return Plot.plot({
+ height: 800,
+ label: null,
+ x: {
+ axis: "top",
+ grid: true,
+ label: "← decrease · Change in population, 2010–2019 (%) · increase →",
+ labelAnchor: "center",
+ tickFormat: "+",
+ percent: true
+ },
+ color: {
+ scheme: "PiYG",
+ type: "ordinal"
+ },
+ marks: [
+ Plot.barX(statepop, {
+ y: "State",
+ x: (d) => change.get(d.State),
+ fill: (d) => Math.sign(change.get(d.State)),
+ sort: {y: "x"}
+ }),
+ Plot.axisY({x: 0, filter: (d) => change.get(d) >= 0, anchor: "left"}),
+ Plot.axisY({x: 0, filter: (d) => change.get(d) < 0, anchor: "right"}),
+ Plot.gridX({stroke: "white", strokeOpacity: 0.5}),
+ Plot.ruleX([0])
+ ]
+ });
+}