Skip to content

Commit 07be0c6

Browse files
committed
derive x & y scale domains from geometry
1 parent 19317ce commit 07be0c6

File tree

9 files changed

+171
-28
lines changed

9 files changed

+171
-28
lines changed

src/dimensions.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function createDimensions(scales, marks, options = {}) {
3838
// specified explicitly, adjust the automatic height accordingly.
3939
let {
4040
width = 640,
41-
height = autoHeight(scales, marks, options, {
41+
height = autoHeight(scales, options, {
4242
width,
4343
marginTopDefault,
4444
marginRightDefault,
@@ -89,14 +89,13 @@ export function createDimensions(scales, marks, options = {}) {
8989

9090
function autoHeight(
9191
{x, y, fy, fx},
92-
marks,
9392
{projection, aspectRatio},
9493
{width, marginTopDefault, marginRightDefault, marginBottomDefault, marginLeftDefault}
9594
) {
9695
const nfy = fy ? fy.scale.domain().length : 1;
9796

9897
// If a projection is specified, use its natural aspect ratio (if known).
99-
const ar = projectionAspectRatio(projection, marks);
98+
const ar = projectionAspectRatio(projection);
10099
if (ar) {
101100
const nfx = fx ? fx.scale.domain().length : 1;
102101
const far = ((1.1 * nfy - 0.1) / (1.1 * nfx - 0.1)) * ar; // 0.1 is default facet padding

src/marks/geo.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class Geo extends Mark {
2222
super(
2323
data,
2424
{
25-
geometry: {value: options.geometry},
25+
geometry: {value: options.geometry, scale: "projection"},
2626
r: {value: vr, scale: "r", filter: positive, optional: true}
2727
},
2828
withDefaultSort(options),

src/plot.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {innerDimensions, outerDimensions} from "./scales.js";
1313
import {position, registry as scaleRegistry} from "./scales/index.js";
1414
import {applyInlineStyles, maybeClassName} from "./style.js";
1515
import {consumeWarnings, warn} from "./warnings.js";
16+
import {getGeometryChannels} from "./projection.js";
1617

1718
export function plot(options = {}) {
1819
const {facet, style, caption, ariaLabel, ariaDescription} = options;
@@ -383,15 +384,26 @@ function addScaleChannels(channelsByScale, stateByMark, filter = yes) {
383384
const channel = channels[name];
384385
const {scale} = channel;
385386
if (scale != null && filter(scale)) {
386-
const scaleChannels = channelsByScale.get(scale);
387-
if (scaleChannels !== undefined) scaleChannels.push(channel);
388-
else channelsByScale.set(scale, [channel]);
387+
if (scale === "projection") {
388+
// TODO only do this if there’s no projection
389+
const [x, y] = getGeometryChannels(channel);
390+
addScaleChannel(channelsByScale, "x", x);
391+
addScaleChannel(channelsByScale, "y", y);
392+
} else {
393+
addScaleChannel(channelsByScale, scale, channel);
394+
}
389395
}
390396
}
391397
}
392398
return channelsByScale;
393399
}
394400

401+
function addScaleChannel(channelsByScale, scale, channel) {
402+
const scaleChannels = channelsByScale.get(scale);
403+
if (scaleChannels !== undefined) scaleChannels.push(channel);
404+
else channelsByScale.set(scale, [channel]);
405+
}
406+
395407
// Returns the facet groups, and possibly fx and fy channels, associated with
396408
// the top-level facet option {data, x, y}.
397409
function maybeTopFacet(facet, options) {
@@ -465,8 +477,8 @@ function inferAxes(marks, channelsByScale, options) {
465477
} = options;
466478

467479
// Disable axes if the corresponding scale is not present.
468-
if (projection || (!isScaleOptions(x) && !hasScaleChannel("x", marks))) xAxis = xGrid = null;
469-
if (projection || (!isScaleOptions(y) && !hasScaleChannel("y", marks))) yAxis = yGrid = null;
480+
if (projection || (!isScaleOptions(x) && !hasPositionChannel("x", marks))) xAxis = xGrid = null;
481+
if (projection || (!isScaleOptions(y) && !hasPositionChannel("y", marks))) yAxis = yGrid = null;
470482
if (!channelsByScale.has("fx")) fxAxis = fxGrid = null;
471483
if (!channelsByScale.has("fy")) fyAxis = fyGrid = null;
472484

@@ -592,10 +604,11 @@ function hasAxis(marks, k) {
592604
return marks.some((m) => m.ariaLabel?.startsWith(prefix));
593605
}
594606

595-
function hasScaleChannel(k, marks) {
607+
function hasPositionChannel(k, marks) {
596608
for (const mark of marks) {
597609
for (const key in mark.channels) {
598-
if (mark.channels[key].scale === k) {
610+
const {scale} = mark.channels[key];
611+
if (scale === k || scale === "projection") {
599612
return true;
600613
}
601614
}

src/projection.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
geoOrthographic,
1515
geoPath,
1616
geoStereographic,
17+
geoStream,
1718
geoTransform,
1819
geoTransverseMercator
1920
} from "d3";
@@ -241,10 +242,10 @@ function project(cx, cy, values, projection) {
241242
// construct the projection), we have to test the raw projection option rather
242243
// than the materialized projection; therefore we must be extremely careful that
243244
// the logic of this function exactly matches Projection above!
244-
export function projectionAspectRatio(projection, marks) {
245+
export function projectionAspectRatio(projection) {
245246
if (typeof projection?.stream === "function") return defaultAspectRatio;
246247
if (isObject(projection)) projection = projection.type;
247-
if (projection == null) return hasGeometry(marks) ? defaultAspectRatio : undefined;
248+
if (projection == null) return;
248249
if (typeof projection !== "function") {
249250
const {aspectRatio} = namedProjection(projection);
250251
if (aspectRatio) return aspectRatio;
@@ -266,7 +267,22 @@ export function applyPosition(channels, scales, context) {
266267
return position;
267268
}
268269

269-
function hasGeometry(marks) {
270-
for (const mark of marks) if (mark.channels.geometry) return true;
271-
return false;
270+
export function getGeometryChannels(channel) {
271+
const X = [];
272+
const Y = [];
273+
const x = {scale: "x", value: X};
274+
const y = {scale: "y", value: Y};
275+
const sink = {
276+
point(x, y) {
277+
X.push(x);
278+
Y.push(y);
279+
},
280+
lineStart() {},
281+
lineEnd() {},
282+
polygonStart() {},
283+
polygonEnd() {},
284+
sphere() {}
285+
};
286+
for (const object of channel.value) geoStream(object, sink);
287+
return [x, y];
272288
}

src/scales/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export const opacity = Symbol("opacity");
2222
// Symbol scales have a default range of categorical symbols.
2323
export const symbol = Symbol("symbol");
2424

25+
// There isn’t really a projection scale; this represents x and y for geometry.
26+
export const projection = Symbol("projection");
27+
2528
// TODO Rather than hard-coding the list of known scale names, collect the names
2629
// and categories for each plot specification, so that custom marks can register
2730
// custom scales.
@@ -34,5 +37,6 @@ export const registry = new Map([
3437
["color", color],
3538
["opacity", opacity],
3639
["symbol", symbol],
37-
["length", length]
40+
["length", length],
41+
["projection", projection]
3842
]);

test/output/geoLine.svg

Lines changed: 65 additions & 0 deletions
Loading

test/output/projectionHeightGeometry.svg

Lines changed: 49 additions & 11 deletions
Loading

test/plots/geo-line.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export async function geoLine() {
5+
const aapl = await d3.csv<any>("data/aapl.csv", d3.autoType);
6+
return Plot.geo({type: "LineString", coordinates: aapl.map((d) => [d.Date, d.Close])}).plot();
7+
}

test/plots/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export * from "./frame.js";
9090
export * from "./fruit-sales-date.js";
9191
export * from "./fruit-sales.js";
9292
export * from "./function-contour.js";
93+
export * from "./geo-line.js";
9394
export * from "./geo-link.js";
9495
export * from "./gistemp-anomaly-moving.js";
9596
export * from "./gistemp-anomaly-transform.js";

0 commit comments

Comments
 (0)