Skip to content

Commit 1c2fd49

Browse files
committed
project x & y; re-fix #1043
1 parent 79d4538 commit 1c2fd49

17 files changed

+264
-28
lines changed

src/channel.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {ascending, descending, rollup, sort} from "d3";
22
import {first, isIterable, labelof, map, maybeValue, range, valueof} from "./options.js";
3+
import {applyProjection} from "./projection.js";
34
import {registry} from "./scales/index.js";
45
import {maybeReduce} from "./transforms/group.js";
56

@@ -24,13 +25,17 @@ export function Channels(descriptors, data) {
2425
}
2526

2627
// TODO Use Float64Array for scales with numeric ranges, e.g. position?
27-
export function valueObject(channels, scales) {
28-
return Object.fromEntries(
28+
export function valueObject(channels, scales, {projection}) {
29+
const values = Object.fromEntries(
2930
Object.entries(channels).map(([name, {scale: scaleName, value}]) => {
3031
const scale = scales[scaleName];
3132
return [name, scale === undefined ? value : map(value, scale)];
3233
})
3334
);
35+
if (projection) {
36+
applyProjection(values, projection);
37+
}
38+
return values;
3439
}
3540

3641
// Note: mutates channel.domain! This is set to a function so that it is lazily

src/marks/delaunay.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class DelaunayLink extends Mark {
6565
markers(this, options);
6666
}
6767
render(index, scales, channels, dimensions, context) {
68+
const {x, y} = scales;
6869
const {x: X, y: Y, z: Z} = channels;
6970
const {curve} = this;
7071
const [cx, cy] = applyFrameAnchor(this, dimensions);
@@ -124,7 +125,7 @@ class DelaunayLink extends Mark {
124125

125126
return create("svg:g", context)
126127
.call(applyIndirectStyles, this, scales, dimensions)
127-
.call(applyTransform, this, scales)
128+
.call(applyTransform, this, {x: X && x, y: Y && y})
128129
.call(
129130
Z
130131
? (g) =>
@@ -155,6 +156,7 @@ class AbstractDelaunayMark extends Mark {
155156
);
156157
}
157158
render(index, scales, channels, dimensions, context) {
159+
const {x, y} = scales;
158160
const {x: X, y: Y, z: Z} = channels;
159161
const [cx, cy] = applyFrameAnchor(this, dimensions);
160162
const xi = X ? (i) => X[i] : constant(cx);
@@ -173,7 +175,7 @@ class AbstractDelaunayMark extends Mark {
173175

174176
return create("svg:g", context)
175177
.call(applyIndirectStyles, this, scales, dimensions)
176-
.call(applyTransform, this, scales)
178+
.call(applyTransform, this, {x: X && x, y: Y && y})
177179
.call(
178180
Z
179181
? (g) =>
@@ -223,6 +225,7 @@ class Voronoi extends Mark {
223225
);
224226
}
225227
render(index, scales, channels, dimensions, context) {
228+
const {x, y} = scales;
226229
const {x: X, y: Y, z: Z} = channels;
227230
const [cx, cy] = applyFrameAnchor(this, dimensions);
228231
const xi = X ? (i) => X[i] : constant(cx);
@@ -243,7 +246,7 @@ class Voronoi extends Mark {
243246

244247
return create("svg:g", context)
245248
.call(applyIndirectStyles, this, scales, dimensions)
246-
.call(applyTransform, this, scales)
249+
.call(applyTransform, this, {x: X && x, y: Y && y})
247250
.call(
248251
Z
249252
? (g) =>

src/marks/density.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class Density extends Mark {
5050
const path = geoPath();
5151
return create("svg:g", context)
5252
.call(applyIndirectStyles, this, scales, dimensions)
53-
.call(applyTransform, this, scales)
53+
.call(applyTransform, this, {})
5454
.call((g) =>
5555
g
5656
.selectAll()

src/marks/dot.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ export class Dot extends Mark {
6060
}
6161
}
6262
render(index, scales, channels, dimensions, context) {
63+
const {x, y} = scales;
6364
const {x: X, y: Y, r: R, rotate: A, symbol: S} = channels;
6465
const [cx, cy] = applyFrameAnchor(this, dimensions);
6566
const circle = this.symbol === symbolCircle;
6667
return create("svg:g", context)
6768
.call(applyIndirectStyles, this, scales, dimensions)
68-
.call(applyTransform, this, scales)
69+
.call(applyTransform, this, {x: X && x, y: Y && y})
6970
.call((g) =>
7071
g
7172
.selectAll()

src/marks/image.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import {
66
applyChannelStyles,
77
applyDirectStyles,
88
applyIndirectStyles,
9-
applyTransform,
109
applyAttr,
1110
impliedString,
12-
applyFrameAnchor
11+
applyFrameAnchor,
12+
applyTransform
1313
} from "../style.js";
1414

1515
const defaults = {
@@ -66,11 +66,12 @@ export class Image extends Mark {
6666
this.frameAnchor = maybeFrameAnchor(frameAnchor);
6767
}
6868
render(index, scales, channels, dimensions, context) {
69+
const {x, y} = scales;
6970
const {x: X, y: Y, width: W, height: H, src: S} = channels;
7071
const [cx, cy] = applyFrameAnchor(this, dimensions);
7172
return create("svg:g", context)
7273
.call(applyIndirectStyles, this, scales, dimensions)
73-
.call(applyTransform, this, scales)
74+
.call(applyTransform, this, {x: X && x, y: Y && y})
7475
.call((g) =>
7576
g
7677
.selectAll()

src/marks/rect.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class Rect extends Mark {
5858
const {insetTop, insetRight, insetBottom, insetLeft, rx, ry} = this;
5959
return create("svg:g", context)
6060
.call(applyIndirectStyles, this, scales, dimensions)
61-
.call(applyTransform, this, {x: X1 && X2 ? x : null, y: Y1 && Y2 ? y : null}, 0, 0)
61+
.call(applyTransform, this, {x: X1 && X2 && x, y: Y1 && Y2 && y}, 0, 0)
6262
.call((g) =>
6363
g
6464
.selectAll()

src/marks/text.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,14 @@ export class Text extends Mark {
8282
this.frameAnchor = maybeFrameAnchor(frameAnchor);
8383
}
8484
render(index, scales, channels, dimensions, context) {
85+
const {x, y} = scales;
8586
const {x: X, y: Y, rotate: R, text: T, fontSize: FS} = channels;
8687
const {rotate} = this;
8788
const [cx, cy] = applyFrameAnchor(this, dimensions);
8889
return create("svg:g", context)
8990
.call(applyIndirectStyles, this, scales, dimensions)
9091
.call(applyIndirectTextStyles, this, T, dimensions)
91-
.call(applyTransform, this, scales)
92+
.call(applyTransform, this, {x: X && x, y: Y && y})
9293
.call((g) =>
9394
g
9495
.selectAll()
@@ -140,7 +141,7 @@ function applyMultilineText(selection, {monospace, lineAnchor, lineHeight, lineW
140141
selection.each(function (i) {
141142
const lines = linesof(formatDefault(T[i]));
142143
const n = lines.length;
143-
const y = lineAnchor === "top" ? 0.71 : lineAnchor === "bottom" ? -0.29 - n : (164 - n * 100) / 200;
144+
const y = lineAnchor === "top" ? 0.71 : lineAnchor === "bottom" ? 1 - n : (164 - n * 100) / 200;
144145
if (n > 1) {
145146
for (let i = 0; i < n; ++i) {
146147
if (!lines[i]) continue;

src/marks/vector.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class Vector extends Mark {
4040
this.frameAnchor = maybeFrameAnchor(frameAnchor);
4141
}
4242
render(index, scales, channels, dimensions, context) {
43+
const {x, y} = scales;
4344
const {x: X, y: Y, length: L, rotate: R} = channels;
4445
const {length, rotate, anchor} = this;
4546
const [cx, cy] = applyFrameAnchor(this, dimensions);
@@ -51,7 +52,7 @@ export class Vector extends Mark {
5152
return create("svg:g", context)
5253
.attr("fill", "none")
5354
.call(applyIndirectStyles, this, scales, dimensions)
54-
.call(applyTransform, this, scales)
55+
.call(applyTransform, this, {x: X && x, y: Y && y})
5556
.call((g) =>
5657
g
5758
.selectAll()

src/plot.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ export function plot(options = {}) {
160160

161161
autoScaleLabels(channelsByScale, scaleDescriptors, axes, dimensions, options);
162162

163-
// Compute value objects, applying scales as needed.
163+
// Compute value objects, applying scales and projection as needed.
164164
for (const state of stateByMark.values()) {
165-
state.values = valueObject(state.channels, scales);
165+
state.values = valueObject(state.channels, scales, context);
166166
}
167167

168168
const {width, height} = dimensions;

src/projection.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,17 @@ export function maybeProjection(projection, dimensions) {
1616
}
1717
throw new Error(`invalid projection: ${projection}`);
1818
}
19+
20+
export function applyProjection(values, projection) {
21+
const {x, y} = values;
22+
if (x && y) {
23+
const n = x.length;
24+
const X = (values.x = new Float64Array(n));
25+
const Y = (values.y = new Float64Array(n));
26+
for (let i = 0; i < n; ++i) {
27+
const p = projection([x[i], y[i]]);
28+
if (p) (X[i] = p[0]), (Y[i] = p[1]);
29+
else X[i] = Y[i] = NaN;
30+
}
31+
}
32+
}

src/scales.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function Scales(
5252
zero,
5353
align,
5454
padding,
55+
projection,
5556
...options
5657
} = {}
5758
) {
@@ -65,6 +66,7 @@ export function Scales(
6566
zero,
6667
align,
6768
padding,
69+
projection,
6870
...scaleOptions
6971
});
7072
if (scale) {
@@ -300,10 +302,15 @@ function formatScaleType(type) {
300302
return typeof type === "symbol" ? type.description : type;
301303
}
302304

303-
function inferScaleType(key, channels, {type, domain, range, scheme, pivot}) {
305+
function inferScaleType(key, channels, {type, domain, range, scheme, pivot, projection}) {
304306
// The facet scales are always band scales; this cannot be changed.
305307
if (key === "fx" || key === "fy") return "band";
306308

309+
// If a projection is specified, the x- and y-scales are disabled; these
310+
// channels will be projected rather than scaled. TODO Throw an error if these
311+
// scales are specified in conjunction with a projection.
312+
if ((key === "x" || key === "y") && projection != null) return;
313+
307314
// If a channel dictates a scale type, make sure that it is consistent with
308315
// the user-specified scale type (if any) and all other channels. For example,
309316
// barY requires x to be a band scale and disallows any other scale type.

test/data/us-state-capitals.csv

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
state,capital,latitude,longitude
2+
Alabama,Montgomery,32.377716,-86.300568
3+
Alaska,Juneau,58.301598,-134.420212
4+
Arizona,Phoenix,33.448143,-112.096962
5+
Arkansas,Little Rock,34.746613,-92.288986
6+
California,Sacramento,38.576668,-121.493629
7+
Colorado,Denver,39.739227,-104.984856
8+
Connecticut,Hartford,41.764046,-72.682198
9+
Delaware,Dover,39.157307,-75.519722
10+
Hawaii,Honolulu,21.307442,-157.857376
11+
Florida,Tallahassee,30.438118,-84.281296
12+
Georgia,Atlanta,33.749027,-84.388229
13+
Idaho,Boise,43.617775,-116.199722
14+
Illinois,Springfield,39.798363,-89.654961
15+
Indiana,Indianapolis,39.768623,-86.162643
16+
Iowa,Des Moines,41.591087,-93.603729
17+
Kansas,Topeka,39.048191,-95.677956
18+
Kentucky,Frankfort,38.186722,-84.875374
19+
Louisiana,Baton Rouge,30.457069,-91.187393
20+
Maine,Augusta,44.307167,-69.781693
21+
Maryland,Annapolis,38.978764,-76.490936
22+
Massachusetts,Boston,42.358162,-71.063698
23+
Michigan,Lansing,42.733635,-84.555328
24+
Minnesota,St. Paul,44.955097,-93.102211
25+
Mississippi,Jackson,32.303848,-90.182106
26+
Missouri,Jefferson City,38.579201,-92.172935
27+
Montana,Helena,46.585709,-112.018417
28+
Nebraska,Lincoln,40.808075,-96.699654
29+
Nevada,Carson City,39.163914,-119.766121
30+
New Hampshire,Concord,43.206898,-71.537994
31+
New Jersey,Trenton,40.220596,-74.769913
32+
New Mexico,Santa Fe,35.68224,-105.939728
33+
North Carolina,Raleigh,35.78043,-78.639099
34+
North Dakota,Bismarck,46.82085,-100.783318
35+
New York,Albany,42.652843,-73.757874
36+
Ohio,Columbus,39.961346,-82.999069
37+
Oklahoma,Oklahoma City,35.492207,-97.503342
38+
Oregon,Salem,44.938461,-123.030403
39+
Pennsylvania,Harrisburg,40.264378,-76.883598
40+
Rhode Island,Providence,41.830914,-71.414963
41+
South Carolina,Columbia,34.000343,-81.033211
42+
South Dakota,Pierre,44.367031,-100.346405
43+
Tennessee,Nashville,36.16581,-86.784241
44+
Texas,Austin,30.27467,-97.740349
45+
Utah,Salt Lake City,40.777477,-111.888237
46+
Vermont,Montpelier,44.262436,-72.580536
47+
Virginia,Richmond,37.538857,-77.43364
48+
Washington,Olympia,47.035805,-122.905014
49+
West Virginia,Charleston,38.336246,-81.612328
50+
Wisconsin,Madison,43.074684,-89.384445
51+
Wyoming,Cheyenne,41.140259,-104.820236

test/output/penguinAnnotated.svg

Lines changed: 1 addition & 1 deletion
Loading

test/output/usStateCapitals.svg

Lines changed: 130 additions & 0 deletions
Loading

test/plots/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ export {default as usPresidentGallery} from "./us-president-gallery.js";
232232
export {default as usPresidentialElection2020} from "./us-presidential-election-2020.js";
233233
export {default as usPresidentialForecast2016} from "./us-presidential-forecast-2016.js";
234234
export {default as usRetailSales} from "./us-retail-sales.js";
235+
export {default as usStateCapitals} from "./us-state-capitals.js";
235236
export {default as usStatePopulationChange} from "./us-state-population-change.js";
236237
export {default as vectorField} from "./vector-field.js";
237238
export {default as vectorFrame} from "./vector-frame.js";

test/plots/penguin-annotated.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,12 @@ export default async function () {
1010
Plot.frame(),
1111
Plot.barX(
1212
penguins,
13-
Plot.groupY(
14-
{x: "count"},
15-
{
16-
y: "species",
17-
fill: "sex",
18-
title: "sex",
19-
sort: {y: "x", reverse: true}
20-
}
21-
)
13+
Plot.groupY({x: "count"}, {y: "species", fill: "sex", title: "sex", sort: {y: "x", reverse: true}})
2214
),
2315
Plot.text(["Count of penguins\ngrouped by species\n and colored by sex"], {
2416
frameAnchor: "bottom-right",
2517
dx: -3,
26-
lineHeight: 1.2,
18+
dy: -3,
2719
fontStyle: "italic"
2820
})
2921
]

test/plots/us-state-capitals.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
import {feature, mesh} from "topojson-client";
4+
5+
export default async function () {
6+
const [[states, statemesh], capitals] = await Promise.all([
7+
d3
8+
.json("data/us-counties-10m.json")
9+
.then((us) => [feature(us, us.objects.states), mesh(us, us.objects.states, (a, b) => a !== b)]),
10+
d3.csv("data/us-state-capitals.csv", d3.autoType)
11+
]);
12+
return Plot.plot({
13+
width: 960,
14+
height: 600,
15+
projection: "albers-usa",
16+
marks: [
17+
Plot.geometry(states, {fill: "#ccc"}),
18+
Plot.geometry(statemesh, {stroke: "white"}),
19+
Plot.dot(capitals, {x: "longitude", y: "latitude", fill: "currentColor"}),
20+
Plot.text(capitals, {
21+
x: "longitude",
22+
y: "latitude",
23+
frameAnchor: "bottom",
24+
text: (d) => `${d.capital}\n${d.state}`,
25+
dy: -6
26+
})
27+
]
28+
});
29+
}

0 commit comments

Comments
 (0)