From 1debd94bf7e9fe7645bb4e2a6ee2f6e46e6a652e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sun, 25 Jun 2023 18:07:50 +0200 Subject: [PATCH 01/27] auto margins alternative to #1714 closes #1451 --- src/dimensions.js | 20 ++++++++++++++++++-- src/mark.js | 4 ++-- src/marks/axis.js | 4 ++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/dimensions.js b/src/dimensions.js index ada0f64787..38f11d10ba 100644 --- a/src/dimensions.js +++ b/src/dimensions.js @@ -1,8 +1,22 @@ -import {extent} from "d3"; +import {max, extent} from "d3"; import {projectionAspectRatio} from "./projection.js"; import {isOrdinalScale} from "./scales.js"; import {offset} from "./style.js"; +// A heuristic to determine the default margin. Ordinal scales usually reclaim +// more space. We can also gauge the “type of contents” (domain, ticks?) and +// decide whether it’s small, medium or large. We don’t want it to match the +// contents exactly because it shouldn’t wobble when the scale changes a little. +function autoMarginH({type, domain, ticks} = {}) { + if (type === "point" || type === "band") return sizeHeuristicH(ticks ?? domain); + return sizeHeuristicH((ticks ?? domain ?? []).map(String)); +} + +function sizeHeuristicH(values = []) { + const l = max(values, (d) => d.length); // TODO text metrics approximation? + return l >= 10 ? 120 : l >= 4 ? 80 : 40; +} + export function createDimensions(scales, marks, options = {}) { // Compute the default margins: the maximum of the marks’ margins. While not // always used, they may be needed to compute the default height of the plot. @@ -11,7 +25,9 @@ export function createDimensions(scales, marks, options = {}) { marginBottomDefault = 0.5 + offset, marginLeftDefault = 0.5 - offset; - for (const {marginTop, marginRight, marginBottom, marginLeft} of marks) { + for (let {marginTop, marginRight, marginBottom, marginLeft} of marks) { + if (marginLeft === "auto") marginLeft = autoMarginH(scales.y); + if (marginRight === "auto") marginRight = autoMarginH(scales.y); if (marginTop > marginTopDefault) marginTopDefault = marginTop; if (marginRight > marginRightDefault) marginRightDefault = marginRight; if (marginBottom > marginBottomDefault) marginBottomDefault = marginBottom; diff --git a/src/mark.js b/src/mark.js index c8c3bca719..ea6f1645da 100644 --- a/src/mark.js +++ b/src/mark.js @@ -67,9 +67,9 @@ export class Mark { this.dx = +dx; this.dy = +dy; this.marginTop = +marginTop; - this.marginRight = +marginRight; + this.marginRight = marginRight === "auto" ? "auto" : +marginRight; this.marginBottom = +marginBottom; - this.marginLeft = +marginLeft; + this.marginLeft = marginLeft === "auto" ? "auto" : +marginLeft; this.clip = maybeClip(clip); this.tip = maybeTip(tip); this.className = className ? maybeClassName(className) : null; diff --git a/src/marks/axis.js b/src/marks/axis.js index 406ff70dd5..0953aff4aa 100644 --- a/src/marks/axis.js +++ b/src/marks/axis.js @@ -82,9 +82,9 @@ function axisKy( x, margin, marginTop = margin === undefined ? 20 : margin, - marginRight = margin === undefined ? (anchor === "right" ? 40 : 0) : margin, + marginRight = margin === undefined ? (anchor === "right" ? "auto" : 0) : margin, marginBottom = margin === undefined ? 20 : margin, - marginLeft = margin === undefined ? (anchor === "left" ? 40 : 0) : margin, + marginLeft = margin === undefined ? (anchor === "left" ? "auto" : 0) : margin, label, labelAnchor, labelArrow, From a4ebdee544fa80334cd19d8110fbd01f90a55737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 27 Jun 2023 14:48:48 +0200 Subject: [PATCH 02/27] progress save --- src/dimensions.js | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/dimensions.js b/src/dimensions.js index 38f11d10ba..804217fa93 100644 --- a/src/dimensions.js +++ b/src/dimensions.js @@ -5,16 +5,29 @@ import {offset} from "./style.js"; // A heuristic to determine the default margin. Ordinal scales usually reclaim // more space. We can also gauge the “type of contents” (domain, ticks?) and -// decide whether it’s small, medium or large. We don’t want it to match the -// contents exactly because it shouldn’t wobble when the scale changes a little. -function autoMarginH({type, domain, ticks} = {}) { - if (type === "point" || type === "band") return sizeHeuristicH(ticks ?? domain); - return sizeHeuristicH((ticks ?? domain ?? []).map(String)); +// decide whether it’s small, medium or large. When the labelAnchor is +// explicitly set to "center", we need more space too. We don’t want the result +// to match the contents exactly because it shouldn’t wobble when the scale +// changes a little. +function autoMarginH({type, domain, ticks, tickFormat} = {}, options) { + if (!type) return 40; + const l = + (type === "point" || type === "band" + ? max(ticks ?? domain ?? [], tickLength) || 0 // TODO text metrics approximation? + : max(ticks ?? domain ?? [], tickLength)) + + 2 * (type === "point" || type === "band" || options?.labelAnchor === "center"); + console.warn(type, tickFormat, domain, l, l >= 10 ? 80 : l > 4 ? 60 : 40, options?.marginLeft); + return l >= 10 ? 80 : l > 4 ? 60 : 40; } -function sizeHeuristicH(values = []) { - const l = max(values, (d) => d.length); // TODO text metrics approximation? - return l >= 10 ? 120 : l >= 4 ? 80 : 40; +function tickLength(value) { + return typeof value === "string" + ? value.length + : typeof value === "number" + ? Math.ceil(Math.abs(Math.log10(Math.abs(value || 2)))) + : value instanceof Date + ? 4 + : 1; } export function createDimensions(scales, marks, options = {}) { @@ -26,8 +39,8 @@ export function createDimensions(scales, marks, options = {}) { marginLeftDefault = 0.5 - offset; for (let {marginTop, marginRight, marginBottom, marginLeft} of marks) { - if (marginLeft === "auto") marginLeft = autoMarginH(scales.y); - if (marginRight === "auto") marginRight = autoMarginH(scales.y); + if (marginLeft === "auto") marginLeft = autoMarginH(scales.y, options.y); + if (marginRight === "auto") marginRight = autoMarginH(scales.y, options.y); if (marginTop > marginTopDefault) marginTopDefault = marginTop; if (marginRight > marginRightDefault) marginRightDefault = marginRight; if (marginBottom > marginBottomDefault) marginBottomDefault = marginBottom; From 2c507363d4324dc6b97b056b6e01448f98340387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 14 Sep 2023 17:20:28 +0200 Subject: [PATCH 03/27] fy (and flipped y/fy) --- src/dimensions.js | 43 ++++++++++++++++++++++-------------------- test/plots/autoplot.ts | 5 +++++ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/dimensions.js b/src/dimensions.js index 804217fa93..812f671b2c 100644 --- a/src/dimensions.js +++ b/src/dimensions.js @@ -2,32 +2,32 @@ import {max, extent} from "d3"; import {projectionAspectRatio} from "./projection.js"; import {isOrdinalScale} from "./scales.js"; import {offset} from "./style.js"; +import {defaultWidth} from "./marks/text.js"; // A heuristic to determine the default margin. Ordinal scales usually reclaim -// more space. We can also gauge the “type of contents” (domain, ticks?) and +// more space. We can also gauge the “type of contents” (domain, ticks) and // decide whether it’s small, medium or large. When the labelAnchor is // explicitly set to "center", we need more space too. We don’t want the result // to match the contents exactly because it shouldn’t wobble when the scale // changes a little. -function autoMarginH({type, domain, ticks, tickFormat} = {}, options) { - if (!type) return 40; +function autoMarginH([scale = {}, options]) { + const marginS = 40; + const marginM = 60; + const marginL = 90; + const {type, ticks, domain} = scale; + if (!type) return marginS; const l = - (type === "point" || type === "band" - ? max(ticks ?? domain ?? [], tickLength) || 0 // TODO text metrics approximation? - : max(ticks ?? domain ?? [], tickLength)) + + (max(ticks ?? domain ?? [], (d) => + typeof d === "string" + ? defaultWidth(d) + : typeof d === "number" + ? 60 * Math.ceil(Math.abs(Math.log10(Math.abs(d || 2)))) + : d instanceof Date + ? 200 + : 60 + ) || 0) + 2 * (type === "point" || type === "band" || options?.labelAnchor === "center"); - console.warn(type, tickFormat, domain, l, l >= 10 ? 80 : l > 4 ? 60 : 40, options?.marginLeft); - return l >= 10 ? 80 : l > 4 ? 60 : 40; -} - -function tickLength(value) { - return typeof value === "string" - ? value.length - : typeof value === "number" - ? Math.ceil(Math.abs(Math.log10(Math.abs(value || 2)))) - : value instanceof Date - ? 4 - : 1; + return l >= 400 ? marginL : l > 240 ? marginM : marginS; } export function createDimensions(scales, marks, options = {}) { @@ -38,9 +38,12 @@ export function createDimensions(scales, marks, options = {}) { marginBottomDefault = 0.5 + offset, marginLeftDefault = 0.5 - offset; + // The left and right margins default to a value inferred from the y (and fy) + // scales, if present. + const yflip = options.y?.axis === "right" || options.fy?.axis === "left"; for (let {marginTop, marginRight, marginBottom, marginLeft} of marks) { - if (marginLeft === "auto") marginLeft = autoMarginH(scales.y, options.y); - if (marginRight === "auto") marginRight = autoMarginH(scales.y, options.y); + if (marginLeft === "auto") marginLeft = autoMarginH(yflip ? [scales.fy, options.fy] : [scales.y, options.y]); + if (marginRight === "auto") marginRight = autoMarginH(yflip ? [scales.y, options.y] : [scales.fy, options.fy]); if (marginTop > marginTopDefault) marginTopDefault = marginTop; if (marginRight > marginRightDefault) marginRightDefault = marginRight; if (marginBottom > marginBottomDefault) marginBottomDefault = marginBottom; diff --git a/test/plots/autoplot.ts b/test/plots/autoplot.ts index 619a8639c6..21f2fb5348 100644 --- a/test/plots/autoplot.ts +++ b/test/plots/autoplot.ts @@ -257,6 +257,11 @@ export async function autoLineFacet() { return Plot.auto(industries, {x: "date", y: "unemployed", fy: "industry"}).plot(); } +export async function autoLineFacetFlip() { + const industries = await d3.csv("data/bls-industry-unemployment.csv", d3.autoType); + return Plot.auto(industries, {x: "date", y: "unemployed", fy: "industry"}).plot({y: {axis: "right"}}); +} + export async function autoDotFacet() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.auto(penguins, {x: "body_mass_g", y: "culmen_length_mm", fx: "island", color: "sex"}).plot(); From 79fa805870f090a114362d89fefa1bced0392bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 14 Sep 2023 17:49:26 +0200 Subject: [PATCH 04/27] impact on tests --- test/output/autoAreaStackColor.svg | 100 +- test/output/autoBarColorReducer.svg | 56 +- test/output/autoBarMode.svg | 42 +- test/output/autoBarStackColor.svg | 66 +- test/output/autoChannels.svg | 22314 ++++++++-------- test/output/autoDotFacet2.svg | 808 +- test/output/autoDotGroup.svg | 40 +- test/output/autoDotOrdCont.svg | 718 +- test/output/autoDotOrdinal.svg | 1658 +- test/output/autoHeatmapOrdCont.svg | 88 +- test/output/autoHeatmapOrdinal.svg | 46 +- test/output/autoLineFacet.svg | 108 +- test/output/autoLineFacetFlip.svg | 278 + test/output/beckerBarley.svg | 2 +- test/output/channelDomainAscending.svg | 148 +- test/output/channelDomainDefault.svg | 148 +- .../output/channelDomainDescendingReverse.svg | 148 +- test/output/channelDomainMinusReverse.svg | 148 +- test/output/crosshairDotFacet.svg | 716 +- test/output/diamondsBoxplot.svg | 9154 +++---- test/output/internFacetNaN.svg | 1066 +- test/output/liborProjectionsFacet.html | 180 +- test/output/mobyDickFaceted.svg | 418 +- test/output/penguinCulmen.svg | 5740 ++-- test/output/penguinCulmenArray.svg | 6400 ++--- test/output/penguinCulmenMarkFacet.svg | 5596 ++-- test/output/penguinFacetAnnotated.svg | 72 +- test/output/penguinMassSex.svg | 2 +- test/output/penguinMassSexSpecies.svg | 176 +- test/output/reducerScaleOverrideFunction.svg | 42 +- .../reducerScaleOverrideImplementation.svg | 42 +- test/output/reducerScaleOverrideName.svg | 42 +- test/output/shorthandCell.svg | 86 +- test/output/sparseTitle.svg | 21802 +++++++-------- test/output/sparseTitleTip.svg | 21802 +++++++-------- test/output/usPopulationStateAgeGrouped.svg | 424 +- test/output/usPresidentialElection2020.svg | 6634 ++--- .../usStatePopulationChangeRelative.svg | 364 +- 38 files changed, 53966 insertions(+), 53708 deletions(-) create mode 100644 test/output/autoLineFacetFlip.svg diff --git a/test/output/autoAreaStackColor.svg b/test/output/autoAreaStackColor.svg index b7b47e58f2..b08ace289c 100644 --- a/test/output/autoAreaStackColor.svg +++ b/test/output/autoAreaStackColor.svg @@ -14,72 +14,62 @@ } - 0 - 2,000 - 4,000 - 6,000 - 8,000 - 10,000 - 12,000 - 14,000 + 0 + 2,000 + 4,000 + 6,000 + 8,000 + 10,000 + 12,000 + 14,000 - - ↑ unemployed + + ↑ unemployed - 2000 - 2001 - 2002 - 2003 - 2004 - 2005 - 2006 - 2007 - 2008 - 2009 - 2010 + 2000 + 2002 + 2004 + 2006 + 2008 + 2010 - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/test/output/autoBarColorReducer.svg b/test/output/autoBarColorReducer.svg index fc39a7d55f..86ff9ee2fa 100644 --- a/test/output/autoBarColorReducer.svg +++ b/test/output/autoBarColorReducer.svg @@ -14,48 +14,48 @@ } - Adelie - Chinstrap - Gentoo + Adelie + Chinstrap + Gentoo - - species + + species - 0 - 20 - 40 - 60 - 80 - 100 - 120 - 140 + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 Frequency → - - - + + + - + \ No newline at end of file diff --git a/test/output/autoBarMode.svg b/test/output/autoBarMode.svg index 33b85940f9..025b8c6f1b 100644 --- a/test/output/autoBarMode.svg +++ b/test/output/autoBarMode.svg @@ -14,35 +14,35 @@ } - Adelie - Chinstrap - Gentoo + Adelie + Chinstrap + Gentoo - - species + + species -