Skip to content

specify grid tick as count or as an array of values #985

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

Closed
wants to merge 2 commits into from
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ Plot automatically generates axes for position scales. You can configure these a
* *scale*.**tickPadding** - the separation between the tick and its label (in pixels; default 3)
* *scale*.**tickFormat** - to format tick values, either a function or format specifier string; see [Formats](#formats)
* *scale*.**tickRotate** - whether to rotate tick labels (an angle in degrees clockwise; default 0)
* *scale*.**grid** - if true, draw grid lines across the plot for each tick
* *scale*.**grid** - if true, a positive number, or an array of tick values, draw grid lines across the plot for each tick; if specified as an array, the default domain includes them
* *scale*.**line** - if true, draw the axis line
* *scale*.**label** - a string to label the axis
* *scale*.**labelAnchor** - the label anchor: *top*, *right*, *bottom*, *left*, or *center*
Expand Down
104 changes: 59 additions & 45 deletions src/axis.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {create} from "./context.js";
import {formatIsoDate} from "./format.js";
import {radians} from "./math.js";
import {boolean, take, number, string, keyword, maybeKeyword, constant, isTemporal} from "./options.js";
import {applyAttr, impliedString} from "./style.js";
import {applyAttr, impliedString, offset} from "./style.js";

export class AxisX {
constructor({
Expand All @@ -30,7 +30,7 @@ export class AxisX {
this.tickPadding = number(tickPadding);
this.tickFormat = maybeTickFormat(tickFormat);
this.fontVariant = impliedString(fontVariant, "normal");
this.grid = boolean(grid);
this.grid = maybeGrid(grid);
this.label = string(label);
this.labelAnchor = maybeKeyword(labelAnchor, "labelAnchor", ["center", "left", "right"]);
this.labelOffset = number(labelOffset);
Expand Down Expand Up @@ -66,23 +66,28 @@ export class AxisX {
labelOffset,
line,
name,
tickRotate
tickRotate,
ticks
} = this;
const offset = name === "x" ? 0 : axis === "top" ? marginTop - facetMarginTop : marginBottom - facetMarginBottom;
const offsetSign = axis === "top" ? -1 : 1;
const ty = offsetSign * offset + (axis === "top" ? marginTop : height - marginBottom);
return create("svg:g", context)
.call(applyAria, this)
.attr("transform", `translate(${offsetLeft},${ty})`)
.call(!grid ? () => {}
: createGridX(
grid(x, ticks),
x,
fy ? fy.bandwidth() : offsetSign * (marginBottom + marginTop - height),
fy ? (index ? take(fy.domain(), index) : fy.domain()).map(d => fy(d) - ty) : [0]
))
.call(createAxis(axis === "top" ? axisTop : axisBottom, x, this))
.call(maybeTickRotate, tickRotate)
.attr("font-size", null)
.attr("font-family", null)
.attr("font-variant", fontVariant)
.call(!line ? g => g.select(".domain").remove() : () => {})
.call(!grid ? () => {}
: fy ? gridFacetX(index, fy, -ty)
: gridX(offsetSign * (marginBottom + marginTop - height)))
.call(!label ? () => {} : g => g.append("text")
.attr("fill", "currentColor")
.attr("transform", `translate(${
Expand Down Expand Up @@ -124,7 +129,7 @@ export class AxisY {
this.tickPadding = number(tickPadding);
this.tickFormat = maybeTickFormat(tickFormat);
this.fontVariant = impliedString(fontVariant, "normal");
this.grid = boolean(grid);
this.grid = maybeGrid(grid);
this.label = string(label);
this.labelAnchor = maybeKeyword(labelAnchor, "labelAnchor", ["center", "top", "bottom"]);
this.labelOffset = number(labelOffset);
Expand Down Expand Up @@ -158,23 +163,28 @@ export class AxisY {
labelOffset,
line,
name,
tickRotate
tickRotate,
ticks
} = this;
const offset = name === "y" ? 0 : axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight;
const offsetSign = axis === "left" ? -1 : 1;
const tx = offsetSign * offset + (axis === "right" ? width - marginRight : marginLeft);
return create("svg:g", context)
.call(applyAria, this)
.attr("transform", `translate(${tx},${offsetTop})`)
.call(!grid ? () => {}
: createGridY(
grid(y, ticks),
y,
fx ? fx.bandwidth() : offsetSign * (marginLeft + marginRight - width),
fx ? (index ? take(fx.domain(), index) : fx.domain()).map(d => fx(d) - tx) : [0]
))
.call(createAxis(axis === "right" ? axisRight : axisLeft, y, this))
.call(maybeTickRotate, tickRotate)
.attr("font-size", null)
.attr("font-family", null)
.attr("font-variant", fontVariant)
.call(!line ? g => g.select(".domain").remove() : () => {})
.call(!grid ? () => {}
: fx ? gridFacetY(index, fx, -tx)
: gridY(offsetSign * (marginLeft + marginRight - width)))
.call(!label ? () => {} : g => g.append("text")
.attr("fill", "currentColor")
.attr("font-variant", fontVariant == null ? null : "normal")
Expand Down Expand Up @@ -204,40 +214,6 @@ function applyAria(selection, {
applyAttr(selection, "aria-description", ariaDescription);
}

function gridX(y2) {
return g => g.selectAll(".tick line")
.clone(true)
.attr("stroke-opacity", 0.1)
.attr("y2", y2);
}

function gridY(x2) {
return g => g.selectAll(".tick line")
.clone(true)
.attr("stroke-opacity", 0.1)
.attr("x2", x2);
}

function gridFacetX(index, fy, ty) {
const dy = fy.bandwidth();
const domain = fy.domain();
return g => g.selectAll(".tick")
.append("path")
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.attr("d", (index ? take(domain, index) : domain).map(v => `M0,${fy(v) + ty}v${dy}`).join(""));
}

function gridFacetY(index, fx, tx) {
const dx = fx.bandwidth();
const domain = fx.domain();
return g => g.selectAll(".tick")
.append("path")
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.attr("d", (index ? take(domain, index) : domain).map(v => `M${fx(v) + tx},0h${dx}`).join(""));
}

function maybeTicks(ticks) {
return ticks === null ? [] : ticks;
}
Expand Down Expand Up @@ -288,3 +264,41 @@ function maybeTickRotate(g, rotate) {
text.setAttribute("dy", "0.32em");
}
}

function createGridX(ticks, x, dy, steps) {
return g => g.append("g")
.attr("class", "grid")
.attr("stroke", "currentColor")
.attr("stroke-opacity", "0.1")
.selectAll()
.data(steps)
.join("g")
.attr("transform", v => `translate(${offset},${v})`)
.selectAll()
.data(ticks)
.join("path")
.attr("d", d => `M${x(d)},0v${dy}`);
}

function createGridY(ticks, y, dx, steps) {
return g => g.append("g")
.attr("class", "grid")
.attr("stroke", "currentColor")
.attr("stroke-opacity", "0.1")
.selectAll()
.data(steps)
.join("g")
.attr("transform", v => `translate(${v},${offset})`)
.selectAll()
.data(ticks)
.join("path")
.attr("d", d => `M0,${y(d)}h${dx}`);
}

function maybeGrid(grid) {
if (!grid) return false;
if (grid === true) return (scale, ticks) => (scale.ticks ? scale.ticks(ticks) : scale.domain());
if (Array.isArray(grid)) return constant(grid.map(d => d == null ? null : +d).filter(d => !isNaN(d)));
if (grid === +grid) return (scale) => (scale.ticks ? scale.ticks.apply(scale, [grid]) : scale.domain());
throw new Error(`Unexpected grid option: ${grid}`);
}
2 changes: 2 additions & 0 deletions src/scales.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function Scales(channelsByScale, {
nice,
clamp,
zero,
grid,
align,
padding,
...options
Expand All @@ -31,6 +32,7 @@ export function Scales(channelsByScale, {
nice,
clamp,
zero,
grid,
align,
padding,
...scaleOptions
Expand Down
6 changes: 4 additions & 2 deletions src/scales/quantitative.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export function ScaleQ(key, scale, channels, {
nice,
clamp,
zero,
domain = inferAutoDomain(key, channels),
grid,
domain = inferAutoDomain(key, channels, grid),
unknown,
round,
scheme,
Expand Down Expand Up @@ -207,8 +208,9 @@ export function inferDomain(channels, f = finite) {
] : [0, 1];
}

function inferAutoDomain(key, channels) {
function inferAutoDomain(key, channels, grid) {
const type = registry.get(key);
if (["x", "y"].includes(key) && Array.isArray(grid)) channels = [...channels, {value: grid}];
return (type === radius || type === opacity || type === length ? inferZeroDomain : inferDomain)(channels);
}

Expand Down
Loading