diff --git a/README.md b/README.md index 6c65e82..876b699 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,12 @@ axis.ticks(10, ",f"); This has the advantage of setting the format precision automatically based on the tick interval. +Multiline tick labels are generated by returning a multiline string—unless the multiline option has been set to false. In multiline mode, each line of the string is placed in a tspan element 1.2em below the preceding line (or above in the case of a top-oriented axis). On each line, the label is suppressed when it repeats the label at the same level on the preceding tick—unless the tickNoRepeat option has been set to false. For example, the following tick format will display a line with the months’ short names, then a line with the year, without repeating the year for two consecutive months: + +```js +axis.tickFormat(d3.utcFormat("%b\n%Y")); +``` + # axis.tickSize([size]) · [Source](https://github.com/d3/d3-axis/blob/master/src/axis.js) If *size* is specified, sets the [inner](#axis_tickSizeInner) and [outer](#axis_tickSizeOuter) tick size to the specified value and returns the axis. If *size* is not specified, returns the current inner tick size, which defaults to 6. @@ -208,3 +214,21 @@ If *padding* is specified, sets the padding to the specified value in pixels and # axis.offset([offset]) · [Source](https://github.com/d3/d3-axis/blob/master/src/axis.js) If *offset* is specified, sets the offset to the specified value in pixels and returns the axis. If *offset* is not specified, returns the current offset which defaults to 0 on devices with a [devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) greater than 1, and 0.5px otherwise. This default offset ensures crisp edges on low-resolution devices. + +# axis.tickMultiline([multiline]) · [Source](https://github.com/d3/d3-axis/blob/master/src/axis.js) + +If *multiline* is specified, sets the multiline behavior to one of: +- *auto*: detects multiline tick labels and wraps each line in a tspan element; +- true: all the labels are wrapped in a tspan element, even when there is only one line +- false: use multiline labels as simple texts. + +If *multiline* is not specified, returns the setting, which defaults to auto. + +# axis.tickNoRepeat([norepeat]) · [Source](https://github.com/d3/d3-axis/blob/master/src/axis.js) + +If *norepeat* is specified, sets the no-repeat behavior to one of: +- true: suppress a tick label that is repeated from the previous tick mark; detection is done line by line. +- false: do not suppress repeating tick labels. + +If *norepeat* is not specified, returns the setting, which defaults to true. + diff --git a/package.json b/package.json index 249f9dc..e669a9b 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "devDependencies": { "d3-scale": "2 - 4", "d3-selection": "1 - 3", + "d3-time-format": "4", "eslint": "7", "js-beautify": "1", "jsdom": "16", diff --git a/src/axis.js b/src/axis.js index 78e1bcb..cde500e 100644 --- a/src/axis.js +++ b/src/axis.js @@ -38,7 +38,9 @@ function axis(orient, scale) { offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5, k = orient === top || orient === left ? -1 : 1, x = orient === left || orient === right ? "x" : "y", - transform = orient === top || orient === bottom ? translateX : translateY; + transform = orient === top || orient === bottom ? translateX : translateY, + multiline = "auto", + norepeat = true; function axis(context) { var values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues, @@ -101,8 +103,7 @@ function axis(orient, scale) { .attr(x + "2", k * tickSizeInner); text - .attr(x, k * spacing) - .text(format); + .call(maybeMultilineText(format, orient, x, k * spacing, multiline, norepeat)); selection.filter(entering) .attr("fill", "none") @@ -134,6 +135,14 @@ function axis(orient, scale) { return arguments.length ? (tickFormat = _, axis) : tickFormat; }; + axis.tickMultiline = function(_) { + return arguments.length ? (multiline = _ === "auto" ? undefined : !!_, axis) : multiline; + } + + axis.tickNoRepeat = function(_) { + return arguments.length ? (norepeat = !!_, axis) : norepeat; + }; + axis.tickSize = function(_) { return arguments.length ? (tickSizeInner = tickSizeOuter = +_, axis) : tickSizeInner; }; @@ -172,3 +181,33 @@ export function axisBottom(scale) { export function axisLeft(scale) { return axis(left, scale); } + +function maybeMultilineText(format, orient, x, space, multiline, norepeat) { + const repeats = []; + const lines = []; + let multi = multiline === "auto" ? false : multiline; + return (text) => { + text + .attr(x, space) + .each(function(_, i) { + const label = format.apply(this, arguments); + const l = label == null + ? [] + : `${label}`.replace(/\s*$/m, "").split(/\r\n?|\n/); + if (l.length > 1 && multiline === "auto") multi = true; + lines[i] = l; + }); + if (multi) { + text + .text("") + .selectAll("tspan") + .data((_, i) => lines[i]) + .join("tspan") + .text((d, j) => norepeat && d === repeats[j] ? "\u00A0" : (repeats[j] = d)) + .attr("x", orient === left || orient === right ? space : 0) + .attr("dy", (_, j) => j === 0 ? null : orient === top ? "-1.2em" : "1.2em"); + } else { + text.text((d, i) => lines[i].join("\n")); + } + }; +} diff --git a/test/snapshots.js b/test/snapshots.js index 7141a9a..4aa998a 100644 --- a/test/snapshots.js +++ b/test/snapshots.js @@ -1,6 +1,7 @@ -import {scaleLinear} from "d3-scale"; +import {scaleLinear, scaleUtc} from "d3-scale"; import {create} from "d3-selection"; -import {axisLeft} from "../src/index.js"; +import {utcFormat} from "d3-time-format"; +import {axisBottom, axisLeft, axisRight, axisTop} from "../src/index.js"; export function axisLeftScaleLinear() { const svg = create("svg"); @@ -13,3 +14,67 @@ export function axisLeftScaleLinearNonNumericRange() { svg.append("g").call(axisLeft(scaleLinear().range([0, "500"]))); return svg.node(); } + +export function axisBottomScaleLinear() { + const x = scaleUtc([new Date(Date.UTC(2000, 4, 1)), new Date(Date.UTC(2002, 10, 1))], [10, 290]); + const axis = axisBottom(x).tickFormat(utcFormat("%b\n")); + const svg = create("svg"); + svg.append("g").call(axis); + return svg.node(); +} + +export function axisBottomScaleLinearMultiLine() { + const x = scaleUtc([new Date(Date.UTC(2000, 4, 1)), new Date(Date.UTC(2002, 10, 1))], [10, 290]); + const axis = axisBottom(x).tickFormat(utcFormat("%b\n%Y")); + const svg = create("svg"); + svg.append("g").call(axis); + return svg.node(); +} + +export function axisBottomScaleLinearQuarters() { + const x = scaleUtc([new Date(Date.UTC(2000, 4, 1)), new Date(Date.UTC(2001, 10, 1))], [10, 290]); + const axis = axisBottom(x).tickFormat(utcFormat("%b\n%Y\nQ%q")); + const svg = create("svg"); + svg.append("g").call(axis); + return svg.node(); +} + +export function axisBottomScaleLinearRepeat() { + const x = scaleUtc([new Date(Date.UTC(2000, 4, 1)), new Date(Date.UTC(2002, 10, 1))], [10, 290]); + const axis = axisBottom(x).tickFormat(utcFormat("%b\n%Y")).tickNoRepeat(false); + const svg = create("svg"); + svg.append("g").call(axis); + return svg.node(); +} + +export function axisBottomScaleLinearNotMultiLine() { + const x = scaleUtc([new Date(Date.UTC(2000, 4, 1)), new Date(Date.UTC(2002, 10, 1))], [10, 290]); + const axis = axisBottom(x).tickFormat(utcFormat("%b\n|")).tickMultiline(false); + const svg = create("svg"); + svg.append("g").call(axis); + return svg.node(); +} + +export function axisTopScaleLinearMultiLine() { + const x = scaleUtc([new Date(Date.UTC(2000, 4, 1)), new Date(Date.UTC(2002, 10, 1))], [10, 290]); + const axis = axisTop(x).tickFormat(utcFormat("%b\n%Y")); + const svg = create("svg"); + svg.append("g").attr("transform", "translate(0,40)").call(axis); + return svg.node(); +} + +export function axisLeftScaleLinearMultiLine() { + const x = scaleUtc([new Date(Date.UTC(2000, 4, 1)), new Date(Date.UTC(2002, 10, 1))], [10, 290]); + const axis = axisLeft(x).tickFormat(utcFormat("%b\n%Y")); + const svg = create("svg"); + svg.append("g").attr("transform", "translate(40,0)").call(axis); + return svg.node(); +} + +export function axisRightScaleLinearMultiLine() { + const x = scaleUtc([new Date(Date.UTC(2000, 4, 1)), new Date(Date.UTC(2002, 10, 1))], [10, 290]); + const axis = axisRight(x).tickFormat(utcFormat("%b\n%Y")); + const svg = create("svg"); + svg.append("g").attr("transform", "translate(40,0)").call(axis); + return svg.node(); +} diff --git a/test/snapshots/axisBottomScaleLinear.html b/test/snapshots/axisBottomScaleLinear.html new file mode 100644 index 0000000..738a8b6 --- /dev/null +++ b/test/snapshots/axisBottomScaleLinear.html @@ -0,0 +1,36 @@ + + + + + + Jul + + + Oct + + + Jan + + + Apr + + + Jul + + + Oct + + + Jan + + + Apr + + + Jul + + + Oct + + + \ No newline at end of file diff --git a/test/snapshots/axisBottomScaleLinearMultiLine.html b/test/snapshots/axisBottomScaleLinearMultiLine.html new file mode 100644 index 0000000..94a2720 --- /dev/null +++ b/test/snapshots/axisBottomScaleLinearMultiLine.html @@ -0,0 +1,66 @@ + + + + + + + Jul + 2000 + + + + + Oct +   + + + + + Jan + 2001 + + + + + Apr +   + + + + + Jul +   + + + + + Oct +   + + + + + Jan + 2002 + + + + + Apr +   + + + + + Jul +   + + + + + Oct +   + + + + \ No newline at end of file diff --git a/test/snapshots/axisBottomScaleLinearNotMultiLine.html b/test/snapshots/axisBottomScaleLinearNotMultiLine.html new file mode 100644 index 0000000..d3a903c --- /dev/null +++ b/test/snapshots/axisBottomScaleLinearNotMultiLine.html @@ -0,0 +1,46 @@ + + + + + + Jul + | + + + Oct + | + + + Jan + | + + + Apr + | + + + Jul + | + + + Oct + | + + + Jan + | + + + Apr + | + + + Jul + | + + + Oct + | + + + \ No newline at end of file diff --git a/test/snapshots/axisBottomScaleLinearQuarters.html b/test/snapshots/axisBottomScaleLinearQuarters.html new file mode 100644 index 0000000..a9b55aa --- /dev/null +++ b/test/snapshots/axisBottomScaleLinearQuarters.html @@ -0,0 +1,48 @@ + + + + + + + Jul + 2000 + Q3 + + + + + Oct +   + Q4 + + + + + Jan + 2001 + Q1 + + + + + Apr +   + Q2 + + + + + Jul +   + Q3 + + + + + Oct +   + Q4 + + + + \ No newline at end of file diff --git a/test/snapshots/axisBottomScaleLinearRepeat.html b/test/snapshots/axisBottomScaleLinearRepeat.html new file mode 100644 index 0000000..dd251f7 --- /dev/null +++ b/test/snapshots/axisBottomScaleLinearRepeat.html @@ -0,0 +1,66 @@ + + + + + + + Jul + 2000 + + + + + Oct + 2000 + + + + + Jan + 2001 + + + + + Apr + 2001 + + + + + Jul + 2001 + + + + + Oct + 2001 + + + + + Jan + 2002 + + + + + Apr + 2002 + + + + + Jul + 2002 + + + + + Oct + 2002 + + + + \ No newline at end of file diff --git a/test/snapshots/axisLeftScaleLinearMultiLine.html b/test/snapshots/axisLeftScaleLinearMultiLine.html new file mode 100644 index 0000000..4bdb6cd --- /dev/null +++ b/test/snapshots/axisLeftScaleLinearMultiLine.html @@ -0,0 +1,66 @@ + + + + + + + Jul + 2000 + + + + + Oct +   + + + + + Jan + 2001 + + + + + Apr +   + + + + + Jul +   + + + + + Oct +   + + + + + Jan + 2002 + + + + + Apr +   + + + + + Jul +   + + + + + Oct +   + + + + \ No newline at end of file diff --git a/test/snapshots/axisRightScaleLinearMultiLine.html b/test/snapshots/axisRightScaleLinearMultiLine.html new file mode 100644 index 0000000..a62f8eb --- /dev/null +++ b/test/snapshots/axisRightScaleLinearMultiLine.html @@ -0,0 +1,66 @@ + + + + + + + Jul + 2000 + + + + + Oct +   + + + + + Jan + 2001 + + + + + Apr +   + + + + + Jul +   + + + + + Oct +   + + + + + Jan + 2002 + + + + + Apr +   + + + + + Jul +   + + + + + Oct +   + + + + \ No newline at end of file diff --git a/test/snapshots/axisTopScaleLinearMultiLine.html b/test/snapshots/axisTopScaleLinearMultiLine.html new file mode 100644 index 0000000..4bb8b53 --- /dev/null +++ b/test/snapshots/axisTopScaleLinearMultiLine.html @@ -0,0 +1,66 @@ + + + + + + + Jul + 2000 + + + + + Oct +   + + + + + Jan + 2001 + + + + + Apr +   + + + + + Jul +   + + + + + Oct +   + + + + + Jan + 2002 + + + + + Apr +   + + + + + Jul +   + + + + + Oct +   + + + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 218a604..722d810 100644 --- a/yarn.lock +++ b/yarn.lock @@ -397,6 +397,13 @@ cssstyle@^2.3.0: dependencies: d3-time "1 - 3" +d3-time-format@4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + "d3-time@1 - 3", "d3-time@2.1.1 - 3": version "3.0.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975"