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 @@ + + + + + + + + + + + + + + + + + + + + + A + B + + + + + + + 1 + 2 + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + −10 + −5 + +0 + +5 + +10 + +15 + + + ← decrease · Change in population, 2010–2019 (%) · increase → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mississippi + New York + Rhode Island + Pennsylvania + New Jersey + Michigan + Maine + Ohio + New Mexico + Kansas + Wisconsin + Missouri + Louisiana + Alabama + Wyoming + Kentucky + Alaska + New Hampshire + Arkansas + Iowa + Indiana + Hawaii + Maryland + Oklahoma + Nebraska + California + Massachusetts + Minnesota + Virginia + Tennessee + Montana + Delaware + South Dakota + Georgia + North Carolina + Oregon + South Carolina + Washington + North Dakota + Arizona + Idaho + Nevada + Florida + Colorado + Texas + Utah + District of Columbia + + + + + + + + + + Puerto Rico + West Virginia + Illinois + Vermont + Connecticut + + + + + + + + + + + + + \ 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]) + ] + }); +}