From a0a3b5054d9566c828a8f9bcba6e3483ded19be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 22 Dec 2021 18:20:53 +0100 Subject: [PATCH] accept axis: "both" for x and y axes note that we avoid showing the label and the grid twice closes #148 --- README.md | 2 +- src/axes.js | 16 +- src/axis.js | 142 ++++--- src/plot.js | 8 +- test/output/carsMpg.svg | 500 +++++++++++++------------ test/output/footballCoverage.svg | 613 ++++++++++++++++--------------- test/output/metroInequality.svg | 608 ++++++++++++++++-------------- test/plots/cars-mpg.js | 3 +- test/plots/football-coverage.js | 3 +- test/plots/metro-inequality.js | 3 +- 10 files changed, 1022 insertions(+), 876 deletions(-) diff --git a/README.md b/README.md index db52169de2..f8514c57d4 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ Align defaults to 0.5 (centered). Band scale padding defaults to 0.1 (10% of ava Plot automatically generates axes for position scales. You can configure these axes with the following options: -* *scale*.**axis** - the orientation: *top* or *bottom* for *x*; *left* or *right* for *y*; null to suppress +* *scale*.**axis** - the orientation: *top*, *bottom* for *fx*, *top*, *bottom*, or *both* for *x*; *left*, *right* for *y*, *left*, *right*, or *both* for *y*; null to suppress * *scale*.**ticks** - the approximate number of ticks to generate * *scale*.**tickSize** - the size of each tick (in pixels; default 6) * *scale*.**tickPadding** - the separation between the tick and its label (in pixels; default 3) diff --git a/src/axes.js b/src/axes.js index 70343a8047..176774636b 100644 --- a/src/axes.js +++ b/src/axes.js @@ -55,20 +55,8 @@ export function autoScaleLabels(channels, scales, {x, y, fx, fy}, dimensions, op fy.labelOffset = fy.axis === "left" ? facetMarginLeft : facetMarginRight; } } - if (x) { - autoAxisLabelsX(x, scales.x, channels.get("x")); - if (x.labelOffset === undefined) { - const {marginTop, marginBottom, facetMarginTop, facetMarginBottom} = dimensions; - x.labelOffset = x.axis === "top" ? marginTop - facetMarginTop : marginBottom - facetMarginBottom; - } - } - if (y) { - autoAxisLabelsY(y, x, scales.y, channels.get("y")); - if (y.labelOffset === undefined) { - const {marginRight, marginLeft, facetMarginLeft, facetMarginRight} = dimensions; - y.labelOffset = y.axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight; - } - } + if (x) autoAxisLabelsX(x, scales.x, channels.get("x")); + if (y) autoAxisLabelsY(y, x, scales.y, channels.get("y")); for (const [key, type] of registry) { if (type !== position && scales[key]) { // not already handled above autoScaleLabel(key, scales[key], channels.get(key), options[key]); diff --git a/src/axis.js b/src/axis.js index eb9c27e389..44aa4ab6b7 100644 --- a/src/axis.js +++ b/src/axis.js @@ -20,7 +20,7 @@ export class AxisX { tickRotate } = {}) { this.name = name; - this.axis = keyword(axis, "axis", ["top", "bottom"]); + this.axis = keyword(axis, "axis", name === "x" ? ["top", "bottom", "both"] : ["top", "bottom"]); this.ticks = ticks; this.tickSize = number(tickSize); this.tickPadding = number(tickPadding); @@ -58,35 +58,48 @@ export class AxisX { labelAnchor, labelOffset, line, + name, tickRotate } = this; - const offset = this.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") - .attr("transform", `translate(0,${ty})`) - .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(${ - labelAnchor === "center" ? (width + marginLeft - marginRight) / 2 - : labelAnchor === "right" ? width + labelMarginRight - : -labelMarginLeft - },${labelOffset * offsetSign})`) - .attr("dy", axis === "top" ? "1em" : "-0.32em") - .attr("text-anchor", labelAnchor === "center" ? "middle" - : labelAnchor === "right" ? "end" - : "start") - .text(label)) - .node(); + const axes = Array.from(axis === "both" ? ["bottom", "top"] : [axis], (axis, secondary) => { + 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") + .attr("transform", `translate(0,${ty})`) + .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 || secondary ? () => {} + : fy ? gridFacetX(index, fy, -ty) + : gridX(offsetSign * (marginBottom + marginTop - height))) + .call(!label || secondary ? () => {} : g => g.append("text") + .attr("fill", "currentColor") + .attr("transform", `translate(${ + labelAnchor === "center" ? (width + marginLeft - marginRight) / 2 + : labelAnchor === "right" ? width + labelMarginRight + : -labelMarginLeft + },${ + offsetSign * (labelOffset !== undefined + ? labelOffset + : axis === "top" ? marginTop - facetMarginTop : marginBottom - facetMarginBottom + )})`) + .attr("dy", axis === "top" ? "1em" : "-0.32em") + .attr("text-anchor", labelAnchor === "center" ? "middle" + : labelAnchor === "right" ? "end" + : "start") + .text(label)) + .node(); + }); + return axis === "both" + ? create("svg:g") + .call(g => g.append(() => axes[0])) + .call(g => g.append(() => axes[1])) + .node() + : axes[0]; } } @@ -107,7 +120,7 @@ export class AxisY { tickRotate } = {}) { this.name = name; - this.axis = keyword(axis, "axis", ["left", "right"]); + this.axis = keyword(axis, "axis", name === "y" ? ["left", "right", "both"] : ["left", "right"]); this.ticks = ticks; this.tickSize = number(tickSize); this.tickPadding = number(tickPadding); @@ -143,37 +156,50 @@ export class AxisY { labelAnchor, labelOffset, line, + name, tickRotate } = this; - const offset = this.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") - .attr("transform", `translate(${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("transform", `translate(${labelOffset * offsetSign},${ - labelAnchor === "center" ? (height + marginTop - marginBottom) / 2 - : labelAnchor === "bottom" ? height - marginBottom - : marginTop - })${labelAnchor === "center" ? ` rotate(-90)` : ""}`) - .attr("dy", labelAnchor === "center" ? (axis === "right" ? "-0.32em" : "0.75em") - : labelAnchor === "bottom" ? "1.4em" - : "-1em") - .attr("text-anchor", labelAnchor === "center" ? "middle" - : axis === "right" ? "end" - : "start") - .text(label)) - .node(); + const axes = Array.from(axis === "both" ? ["right", "left"] : [axis], (axis, secondary) => { + 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") + .attr("transform", `translate(${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 || secondary ? () => {} + : fx ? gridFacetY(index, fx, -tx) + : gridY(offsetSign * (marginLeft + marginRight - width))) + .call(!label || secondary ? () => {} : g => g.append("text") + .attr("fill", "currentColor") + .attr("transform", `translate(${ + offsetSign * (labelOffset !== undefined + ? labelOffset + : axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight + )},${ + labelAnchor === "center" ? (height + marginTop - marginBottom) / 2 + : labelAnchor === "bottom" ? height - marginBottom + : marginTop + })${labelAnchor === "center" ? ` rotate(-90)` : ""}`) + .attr("dy", labelAnchor === "center" ? (axis === "right" ? "-0.32em" : "0.75em") + : labelAnchor === "bottom" ? "1.4em" + : "-1em") + .attr("text-anchor", labelAnchor === "center" ? "middle" + : axis === "right" ? "end" + : "start") + .text(label)) + .node(); + }); + return axis === "both" + ? create("svg:g") + .call(g => g.append(() => axes[0])) + .call(g => g.append(() => axes[1])) + .node() + : axes[0]; } } diff --git a/src/plot.js b/src/plot.js index d83e5715e7..567d34e7b0 100644 --- a/src/plot.js +++ b/src/plot.js @@ -135,10 +135,10 @@ function Dimensions( marginLeft: facetMarginLeft = facetMargin !== undefined ? facetMargin : fyAxis === "left" ? 40 : 0 } = {}, margin, - marginTop = margin !== undefined ? margin : Math.max((xAxis === "top" ? 30 : 0) + facetMarginTop, yAxis || fyAxis ? 20 : 0.5 - offset), - marginRight = margin !== undefined ? margin : Math.max((yAxis === "right" ? 40 : 0) + facetMarginRight, xAxis || fxAxis ? 20 : 0.5 + offset), - marginBottom = margin !== undefined ? margin : Math.max((xAxis === "bottom" ? 30 : 0) + facetMarginBottom, yAxis || fyAxis ? 20 : 0.5 + offset), - marginLeft = margin !== undefined ? margin : Math.max((yAxis === "left" ? 40 : 0) + facetMarginLeft, xAxis || fxAxis ? 20 : 0.5 - offset) + marginTop = margin !== undefined ? margin : Math.max((xAxis === "top" || xAxis === "both" ? 30 : 0) + facetMarginTop, yAxis || fyAxis ? 20 : 0.5 - offset), + marginRight = margin !== undefined ? margin : Math.max((yAxis === "right" || yAxis === "both" ? 40 : 0) + facetMarginRight, xAxis || fxAxis ? 20 : 0.5 + offset), + marginBottom = margin !== undefined ? margin : Math.max((xAxis === "bottom" || xAxis === "both" ? 30 : 0) + facetMarginBottom, yAxis || fyAxis ? 20 : 0.5 + offset), + marginLeft = margin !== undefined ? margin : Math.max((yAxis === "left" || yAxis === "both" ? 40 : 0) + facetMarginLeft, xAxis || fxAxis ? 20 : 0.5 - offset) } = {} ) { return { diff --git a/test/output/carsMpg.svg b/test/output/carsMpg.svg index f0de5307f1..565663bf2e 100644 --- a/test/output/carsMpg.svg +++ b/test/output/carsMpg.svg @@ -12,271 +12,305 @@ white-space: pre; } - - - - 0 + + + + + 0 + + + + 5 + + + + 10 + + + + 15 + + + + 20 + + + + 25 + + + + 30 + + + + 35 + + + + 40 + + + + 45 + ↑ economy (mpg) - - - 5 + + + 0 + + + 5 + + + 10 + + + 15 + + + 20 + + + 25 + + + 30 + + + 35 + + + 40 + + + 45 + - - - 10 - - - - 15 - - - - 20 - - - - 25 - - - - 30 - - - - 35 - - - - 40 - - - - 45 - ↑ economy (mpg) - + 70 - + 71 - + 72 - + 73 - + 74 - + 75 - + 76 - + 77 - + 78 - + 79 - + 80 - + 81 - + 82 - year + year - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/footballCoverage.svg b/test/output/footballCoverage.svg index 8f7267f3b0..b2e11f8297 100644 --- a/test/output/footballCoverage.svg +++ b/test/output/footballCoverage.svg @@ -12,333 +12,370 @@ white-space: pre; } - - - 0% - - - - 5% - - - - 10% - - - - 15% - - - - 20% - - - - 25% - - - - 30% - - - - 35% - - - - 40% - - - - 45% - + + + + 0% + + + + 5% + + + + 10% + + + + 15% + + + + 20% + + + + 25% + + + + 30% + + + + 35% + + + + 40% + + + + 45% + + + + 50% + + - - 50% - + + + 0% + + + 5% + + + 10% + + + 15% + + + 20% + + + 25% + + + 30% + + + 35% + + + 40% + + + 45% + + + 50% + - + C2 - + C3 - + C4 - + M0 - + M1 - + M2 - + T2 - coverage + coverage - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/output/metroInequality.svg b/test/output/metroInequality.svg index 55e7872a8e..bf6d29f9ff 100644 --- a/test/output/metroInequality.svg +++ b/test/output/metroInequality.svg @@ -13,324 +13,382 @@ } - + 3.4 - + 3.6 - + 3.8 - + 4.0 - + 4.2 - + 4.4 - + 4.6 - + 4.8 - + 5.0 - + 5.2 - + 5.4 - + 5.6 - ↑ Inequality + ↑ Inequality - - - - 200k + + + + + 200k + + + + 300k + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1M + + + + 2M + + + + 3M + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10M + Population → - - - 300k + + + 200k + + + 300k + + + + + + + + + + + + + + + + + + + + + 1M + + + 2M + + + 3M + + + + + + + + + + + + + + + + + + + + + 10M + - - - - - - - - - - - - - - - - - - - - - - - - - - - 1M - - - - 2M - - - - 3M - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10M - Population → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - + + + + + + \ No newline at end of file diff --git a/test/plots/cars-mpg.js b/test/plots/cars-mpg.js index 6ff72d6a4a..06cd9dbdbb 100644 --- a/test/plots/cars-mpg.js +++ b/test/plots/cars-mpg.js @@ -9,7 +9,8 @@ export default async function() { }, y: { grid: true, - zero: true + zero: true, + axis: "both" }, color: { type: "ordinal" diff --git a/test/plots/football-coverage.js b/test/plots/football-coverage.js index 9d54d9dde7..c175241835 100644 --- a/test/plots/football-coverage.js +++ b/test/plots/football-coverage.js @@ -10,7 +10,8 @@ export default async function() { y: { grid: true, domain: [0, 0.5], - tickFormat: "%" + tickFormat: "%", + axis: "both" }, facet: { data: football, diff --git a/test/plots/metro-inequality.js b/test/plots/metro-inequality.js index 4ac0baac7c..18339ca5c1 100644 --- a/test/plots/metro-inequality.js +++ b/test/plots/metro-inequality.js @@ -8,7 +8,8 @@ export default async function() { inset: 10, x: { type: "log", - label: "Population →" + label: "Population →", + axis: "both" }, y: { label: "↑ Inequality"