Skip to content
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

labelArrow #702

Closed
wants to merge 1 commit 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
83 changes: 46 additions & 37 deletions src/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function autoScaleLabels(channels, scales, {x, y, fx, fy}, dimensions, op
}
for (const [key, type] of registry) {
if (type !== position && scales[key]) { // not already handled above
autoScaleLabel(key, scales[key], channels.get(key), options[key]);
autoScaleLabel(scales[key], channels.get(key), options[key]);
}
}
}
Expand All @@ -83,10 +83,8 @@ function autoAxisLabelsX(axis, scale, channels) {
: scaleOrder(scale) < 0 ? "left"
: "right";
}
if (axis.label === undefined) {
axis.label = inferLabel(channels, scale, axis, "x");
}
scale.label = axis.label;
autoScaleLabel(scale, channels, axis);
autoAxisLabel("x", scale, axis);
}

// Mutates axis.labelAnchor, axis.label, scale.label!
Expand All @@ -96,51 +94,62 @@ function autoAxisLabelsY(axis, opposite, scale, channels) {
: opposite && opposite.axis === "top" ? "bottom" // TODO scaleOrder?
: "top";
}
if (axis.label === undefined) {
axis.label = inferLabel(channels, scale, axis, "y");
}
scale.label = axis.label;
autoScaleLabel(scale, channels, axis);
autoAxisLabel("y", scale, axis);
}

// Mutates scale.label!
function autoScaleLabel(key, scale, channels, options) {
if (options) {
scale.label = options.label;
}
// Mutates scale.label, axis.labelArrow!
function autoScaleLabel(scale, channels, axis) {
if (axis) scale.label = axis.label; // propagate manual label, if any, to scale
if (scale.label === undefined) {
scale.label = inferLabel(channels, scale, null, key);
scale.label = inferLabel(channels, scale);
if (axis) axis.label = scale.label; // propagate automatic label, if used, to axis
} else if (axis?.labelArrow === "auto") {
axis.labelArrow = "none"; // clear default label arrow if explicit label
}
}

// Mutates axis.label!
function autoAxisLabel(key, scale, axis) {
let {label, labelAnchor, labelArrow} = axis;
if (label != null) {
if (labelArrow === "auto" && !isOrdinalScale(scale)) {
const order = scaleOrder(scale);
if (order) {
if (key === "x" || labelAnchor === "center") {
labelArrow = key === "x" === order < 0 ? "left" : "right";
} else {
labelArrow = order < 0 ? "up" : "down";
}
}
}
switch (labelArrow) {
case "left": label = `← ${label}`; break;
case "right": label = `${label} →`; break;
case "up": label = `↑ ${label}`; break;
case "down": label = `↓ ${label}`; break;
}
axis.label = label;
}
}

// Channels can have labels; if all the channels for a given scale are
// consistently labeled (i.e., have the same value if not undefined), and the
// corresponding axis doesn’t already have an explicit label, then the channels’
// label is promoted to the corresponding axis.
function inferLabel(channels = [], scale, axis, key) {
let candidate;
for (const {label} of channels) {
if (label === undefined) continue;
if (candidate === undefined) candidate = label;
else if (candidate !== label) return;
function inferLabel(channels = [], scale) {
let label;
for (const {label: channelLabel} of channels) {
if (channelLabel === undefined) continue;
if (label === undefined) label = channelLabel;
else if (label !== channelLabel) return;
}
if (candidate !== undefined) {
if (label !== undefined) {
// Ignore the implicit label for temporal scales if it’s simply “date”.
if (isTemporalScale(scale) && /^(date|time|year)$/i.test(candidate)) return;
if (!isOrdinalScale(scale)) {
if (scale.percent) candidate = `${candidate} (%)`;
if (key === "x" || key === "y") {
const order = scaleOrder(scale);
if (order) {
if (key === "x" || (axis && axis.labelAnchor === "center")) {
candidate = key === "x" === order < 0 ? `← ${candidate}` : `${candidate} →`;
} else {
candidate = `${order < 0 ? "↑ " : "↓ "}${candidate}`;
}
}
}
}
if (isTemporalScale(scale) && /^(date|time|year)$/i.test(label)) return;
if (!isOrdinalScale(scale) && scale.percent) label = `${label} (%)`;
}
return candidate;
return label;
}

export function inferFontVariant(scale) {
Expand Down
10 changes: 10 additions & 0 deletions src/axis.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import {formatIsoDate} from "./format.js";
import {radians} from "./math.js";
import {impliedString} from "./style.js";

function maybeLabelArrow(labelArrow = true) {
return labelArrow === true ? "auto"
: labelArrow === false || labelArrow == null ? "none"
: keyword(labelArrow, "labelArrow", ["auto", "none", "up", "down", "left", "right"]);
}

export class AxisX {
constructor({
name = "x",
Expand All @@ -16,6 +22,7 @@ export class AxisX {
grid,
label,
labelAnchor,
labelArrow,
labelOffset,
line,
tickRotate
Expand All @@ -30,6 +37,7 @@ export class AxisX {
this.grid = boolean(grid);
this.label = string(label);
this.labelAnchor = maybeKeyword(labelAnchor, "labelAnchor", ["center", "left", "right"]);
this.labelArrow = maybeLabelArrow(labelArrow);
this.labelOffset = number(labelOffset);
this.line = boolean(line);
this.tickRotate = number(tickRotate);
Expand Down Expand Up @@ -103,6 +111,7 @@ export class AxisY {
grid,
label,
labelAnchor,
labelArrow,
labelOffset,
line,
tickRotate
Expand All @@ -117,6 +126,7 @@ export class AxisY {
this.grid = boolean(grid);
this.label = string(label);
this.labelAnchor = maybeKeyword(labelAnchor, "labelAnchor", ["center", "top", "bottom"]);
this.labelArrow = maybeLabelArrow(labelArrow);
this.labelOffset = number(labelOffset);
this.line = boolean(line);
this.tickRotate = number(tickRotate);
Expand Down
3 changes: 2 additions & 1 deletion test/plots/aapl-candlestick.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export default async function() {
inset: 6,
grid: true,
y: {
label: "↑ Apple stock price ($)",
label: "Apple stock price ($)",
labelArrow: "up",
fontVariant: null
},
color: {
Expand Down
26 changes: 13 additions & 13 deletions test/scales/scales-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ it("plot(…).scale('x') returns the expected linear scale for penguins", async
range: [20, 620],
interpolate: d3.interpolateNumber,
clamp: false,
label: "body_mass_g"
label: "body_mass_g"
});
});

Expand Down Expand Up @@ -307,7 +307,7 @@ it("plot(…).scale(name) promotes the given zero option to the domain", async (
range: [20, 620],
interpolate: d3.interpolateNumber,
clamp: false,
label: "body_mass_g"
label: "body_mass_g"
});
});

Expand All @@ -320,7 +320,7 @@ it("plot(…).scale(name) handles the zero option correctly for descending domai
range: [20, 620],
interpolate: d3.interpolateNumber,
clamp: false,
label: "body_mass_g"
label: "body_mass_g"
});
});

Expand All @@ -333,7 +333,7 @@ it("plot(…).scale(name) handles the zero option correctly for polylinear domai
range: [20, 320, 620],
interpolate: d3.interpolateNumber,
clamp: false,
label: "body_mass_g"
label: "body_mass_g"
});
});

Expand All @@ -346,7 +346,7 @@ it("plot(…).scale(name) handles the zero option correctly for descending polyl
range: [20, 320, 620],
interpolate: d3.interpolateNumber,
clamp: false,
label: "body_mass_g"
label: "body_mass_g"
});
});

Expand Down Expand Up @@ -1174,13 +1174,13 @@ it("plot({padding, …}).scale('x').padding reflects the given padding option fo
});

it("plot(…).scale('x').label reflects the default label for named fields, possibly reversed", () => {
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: "foo"}).plot().scale("x").label, "foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: "foo"}).plot({x: {reverse: true}}).scale("x").label, "foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: "foo"}).plot().scale("x").label, "foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: "foo"}).plot({x: {reverse: true}}).scale("x").label, "foo");
});

it("plot(…).scale('y').label reflects the default label for named fields, possibly reversed", () => {
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {y: "foo"}).plot().scale("y").label, "foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {y: "foo"}).plot({y: {reverse: true}}).scale("y").label, "foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {y: "foo"}).plot().scale("y").label, "foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {y: "foo"}).plot({y: {reverse: true}}).scale("y").label, "foo");
});

it("plot(…).scale('x').label reflects the explicit label", () => {
Expand All @@ -1190,13 +1190,13 @@ it("plot(…).scale('x').label reflects the explicit label", () => {

it("plot(…).scale('x').label reflects a function label, if not overridden by an explicit label", () => {
const foo = Object.assign(d => d.foo, {label: "Foo"});
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: foo}).plot().scale("x").label, "Foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: foo}).plot().scale("x").label, "Foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: foo}).plot({x: {label: null}}).scale("x").label, null);
});

it("plot(…).scale('x').label reflects a channel transform label, if not overridden by an explicit label", () => {
const foo = {transform: data => data.map(d => d.foo), label: "Foo"};
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: foo}).plot().scale("x").label, "Foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: foo}).plot().scale("x").label, "Foo");
assert.strictEqual(Plot.dot([{foo: 1}, {foo: 2}, {foo: 3}], {x: foo}).plot({x: {label: null}}).scale("x").label, null);
});

Expand Down Expand Up @@ -1305,7 +1305,7 @@ it("plot(…).scale(name) reflects the given custom interpolator", async () => {
range: [20, 620],
interpolate,
clamp: false,
label: "body_mass_g"
label: "body_mass_g"
});
});

Expand Down Expand Up @@ -1362,7 +1362,7 @@ it("plot(…).scale(name) reflects the given transform", async () => {
clamp: false,
interpolate: d3.interpolateNumber,
transform,
label: "body_mass_g"
label: "body_mass_g"
});
});

Expand Down