Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Axes and grids now support the filter option (#1665)
Browse files Browse the repository at this point in the history
* Axes and grids now support {filter, sort, reverse}

closes #1457
closes #1655
Fil committed Aug 21, 2023

Verified

This commit was signed with the committer’s verified signature.
DanielaE Daniela Engert
1 parent 9735f6f commit 52f1c98
Showing 8 changed files with 817 additions and 505 deletions.
90 changes: 46 additions & 44 deletions src/marks/axis.js
Original file line number Diff line number Diff line change
@@ -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 = {};
45 changes: 45 additions & 0 deletions test/output/axisFilter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
900 changes: 449 additions & 451 deletions test/output/trafficHorizon.html

Large diffs are not rendered by default.

220 changes: 220 additions & 0 deletions test/output/usStatePopulationChangeRelative.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions test/plots/axis-filter.ts
Original file line number Diff line number Diff line change
@@ -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})
]
});
}
1 change: 1 addition & 0 deletions test/plots/index.ts
Original file line number Diff line number Diff line change
@@ -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";
15 changes: 5 additions & 10 deletions test/plots/traffic-horizon.ts
Original file line number Diff line number Diff line change
@@ -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
]
});
}
33 changes: 33 additions & 0 deletions test/plots/us-state-population-change.ts
Original file line number Diff line number Diff line change
@@ -33,3 +33,36 @@ export async function usStatePopulationChange() {
]
});
}

export async function usStatePopulationChangeRelative() {
const statepop = await d3.csv<any>("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])
]
});
}

0 comments on commit 52f1c98

Please sign in to comment.