Skip to content

Commit 2872a89

Browse files
committed
projection autoheight
1 parent d70fd8b commit 2872a89

File tree

3 files changed

+36
-17
lines changed

3 files changed

+36
-17
lines changed

src/dimensions.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {hasProjection} from "./projection.js";
1+
import {projectionAspectRatio} from "./projection.js";
22
import {isOrdinalScale} from "./scales.js";
33
import {offset} from "./style.js";
44

@@ -52,7 +52,7 @@ export function Dimensions(scales, geometry, axes, options = {}) {
5252
// specified explicitly, adjust the automatic height accordingly.
5353
let {
5454
width = 640,
55-
height = autoHeight(scales, geometry || hasProjection(options)) +
55+
height = autoHeight(scales, projectionAspectRatio(options, geometry), width - marginLeft - marginRight) +
5656
Math.max(0, marginTop - marginTopDefault + marginBottom - marginBottomDefault)
5757
} = options;
5858

@@ -74,8 +74,17 @@ export function Dimensions(scales, geometry, axes, options = {}) {
7474
};
7575
}
7676

77-
function autoHeight({y, fy, fx}, geometry) {
77+
function autoHeight({y, fy, fx}, projectionRatio, width) {
78+
if (projectionRatio > 0) {
79+
return Math.max(
80+
200,
81+
Math.min(
82+
1200,
83+
Math.ceil((fx ? fx.scale.bandwidth() : 1) * width * projectionRatio) / (fy ? fy.scale.bandwidth() : 1)
84+
)
85+
);
86+
}
7887
const nfy = fy ? fy.scale.domain().length : 1;
79-
const ny = y ? (isOrdinalScale(y) ? y.scale.domain().length : Math.max(7, 17 / nfy)) : geometry ? 17 : 1;
80-
return !!(y || fy || geometry) * Math.max(1, Math.min(60, ny * nfy)) * 20 + !!fx * 30 + 60;
88+
const ny = y ? (isOrdinalScale(y) ? y.scale.domain().length : Math.max(7, 17 / nfy)) : 1;
89+
return !!(y || fy) * Math.max(1, Math.min(60, ny * nfy)) * 20 + !!fx * 30 + 60;
8190
}

src/projection.js

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import {
2020
import {constant, isObject} from "./options.js";
2121
import {warn} from "./warnings.js";
2222

23+
const pi = Math.PI;
24+
const tau = 2 * pi;
25+
const golden = 1.618;
26+
2327
export function Projection(
2428
{
2529
projection,
@@ -76,6 +80,7 @@ export function Projection(
7680
let transform;
7781

7882
// If a domain is specified, fit the projection to the frame.
83+
let ratio = projection.ratio;
7984
if (domain != null) {
8085
const [[x0, y0], [x1, y1]] = geoPath(projection).bounds(domain);
8186
const k = Math.min(dx / (x1 - x0), dy / (y1 - y0));
@@ -87,6 +92,7 @@ export function Projection(
8792
this.stream.point(x * k + tx, y * k + ty);
8893
}
8994
});
95+
ratio = Math.max(0.5, Math.min(golden, (y1 - y0) / (x1 - x0)));
9096
} else {
9197
warn(`Warning: the projection could not be fit to the specified domain; using the default scale.`);
9298
}
@@ -101,20 +107,21 @@ export function Projection(
101107
}
102108
});
103109

104-
return {stream: (s) => projection.stream(transform.stream(clip(s)))};
110+
return {stream: (s) => projection.stream(transform.stream(clip(s))), ratio};
105111
}
106112

107-
export function hasProjection({projection} = {}) {
108-
if (projection == null) return false;
109-
if (typeof projection.stream === "function") return true; // d3 projection
110-
if (isObject(projection)) ({type: projection} = projection);
111-
if (typeof projection !== "function") projection = namedProjection(projection);
112-
return projection != null;
113+
// When a projection is specified, try to determine a good value for the
114+
// projection’s height, if it is a named projection. When we don’t have a way to
115+
// know, the golden ratio is our best guess.
116+
export function projectionAspectRatio({projection} = {}, geometry) {
117+
projection = Projection(
118+
{projection},
119+
{width: 100, height: 300, marginLeft: 0, marginRight: 0, marginTop: 0, marginBottom: 0}
120+
);
121+
if (projection == null) return geometry ? golden - 1 : 0;
122+
return projection.ratio ?? golden - 1;
113123
}
114124

115-
const pi = Math.PI;
116-
const tau = 2 * pi;
117-
118125
function namedProjection(projection) {
119126
switch (`${projection}`.toLowerCase()) {
120127
case "albers-usa":
@@ -142,7 +149,7 @@ function namedProjection(projection) {
142149
case "reflect-y":
143150
return reflectY;
144151
case "mercator":
145-
return scaleProjection(geoMercator, tau, tau);
152+
return scaleProjection(geoMercator, tau, tau, 1);
146153
case "orthographic":
147154
return scaleProjection(geoOrthographic, 2, 2);
148155
case "stereographic":
@@ -165,14 +172,15 @@ function maybePostClip(clip, x1, y1, x2, y2) {
165172
}
166173
}
167174

168-
function scaleProjection(createProjection, kx, ky) {
175+
function scaleProjection(createProjection, kx, ky, ratio) {
169176
return ({width, height, rotate, precision = 0.15, clip}) => {
170177
const projection = createProjection();
171178
if (precision != null) projection.precision?.(precision);
172179
if (rotate != null) projection.rotate?.(rotate);
173180
if (typeof clip === "number") projection.clipAngle?.(clip);
174181
projection.scale(Math.min(width / kx, height / ky));
175182
projection.translate([width / 2, height / 2]);
183+
if (ratio > 0) projection.ratio = ratio;
176184
return projection;
177185
};
178186
}

test/plots/projection-fit-conic.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export default async function () {
66
const world = await d3.json("data/countries-110m.json");
77
const land = feature(world, world.objects.land);
88
return Plot.plot({
9+
width: 640,
10+
height: 400,
911
projection: {
1012
type: "conic-equal-area",
1113
parallels: [-42, -5],

0 commit comments

Comments
 (0)