diff --git a/docs/marks/geo.md b/docs/marks/geo.md
index 0d35629d6e..56748f0027 100644
--- a/docs/marks/geo.md
+++ b/docs/marks/geo.md
@@ -49,7 +49,8 @@ Plot.plot({
marks: [
Plot.geo(counties, {
fill: (d) => d.properties.unemployment,
- title: (d) => `${d.properties.name}\n${d.properties.unemployment}%`
+ title: (d) => `${d.properties.name} ${d.properties.unemployment}%`,
+ tip: true
})
]
})
@@ -129,17 +130,16 @@ Plot.plot({
```
:::
-The geo mark doesn’t have **x** and **y** channels; to derive those, for example to add [interactive tips](./tip.md), you can apply a [centroid transform](../transforms/centroid.md) on the geometries.
+By default, the geo mark doesn’t have **x** and **y** channels; when you use the [**tip** option](./tip.md), the [centroid transform](../transforms/centroid.md) is implicitly applied on the geometries to compute the tip position by generating **x** and **y** channels. You can alternatively specify these channels explicitly. The centroids are shown below in red.
:::plot defer https://observablehq.com/@observablehq/plot-state-centroids
```js
Plot.plot({
projection: "albers-usa",
marks: [
- Plot.geo(statemesh, {strokeOpacity: 0.2}),
+ Plot.geo(states, {strokeOpacity: 0.1, tip: true, title: (d) => d.properties.name}),
Plot.geo(nation),
- Plot.dot(states, Plot.centroid({fill: "red", stroke: "var(--vp-c-bg-alt)"})),
- Plot.tip(states, Plot.pointer(Plot.centroid({title: (d) => d.properties.name})))
+ Plot.dot(states, Plot.centroid({fill: "red", stroke: "var(--vp-c-bg-alt)"}))
]
})
```
@@ -157,7 +157,7 @@ Plot.plot({
marks: [
Plot.geo(statemesh, {strokeOpacity: 0.2}),
Plot.geo(nation),
- Plot.geo(walmarts, {fy: (d) => d.properties.date, r: 1.5, fill: "blue"}),
+ Plot.geo(walmarts, {fy: (d) => d.properties.date, r: 1.5, fill: "blue", tip: true, title: (d) => d.properties.date}),
Plot.axisFy({frameAnchor: "top", dy: 30, tickFormat: (d) => `${d.getUTCFullYear()}’s`})
]
})
@@ -176,6 +176,8 @@ The **geometry** channel specifies the geometry (GeoJSON object) to draw; if not
In addition to the [standard mark options](../features/marks.md#mark-options), the **r** option controls the size of Point and MultiPoint geometries. It can be specified as either a channel or constant. When **r** is specified as a number, it is interpreted as a constant radius in pixels; otherwise it is interpreted as a channel and the effective radius is controlled by the *r* scale. If the **r** option is not specified it defaults to 3 pixels. Geometries with a nonpositive radius are not drawn. If **r** is a channel, geometries will be sorted by descending radius by default.
+The **x** and **y** position channels may also be specified in conjunction with the **tip** option. These are bound to the *x* and *y* scale (or projection), respectively.
+
## geo(*data*, *options*) {#geo}
```js
diff --git a/src/marks/geo.d.ts b/src/marks/geo.d.ts
index d37a23e7fe..035d774c69 100644
--- a/src/marks/geo.d.ts
+++ b/src/marks/geo.d.ts
@@ -10,6 +10,20 @@ export interface GeoOptions extends MarkOptions {
*/
geometry?: ChannelValue;
+ /**
+ * In conjunction with the tip option, the horizontal position channel
+ * specifying the tip’s anchor, typically bound to the *x* scale. If not
+ * specified, defaults to the centroid of the projected geometry.
+ */
+ x?: ChannelValue;
+
+ /**
+ * In conjunction with the tip option, the vertical position channel
+ * specifying the tip’s anchor, typically bound to the *y* scale. If not
+ * specified, defaults to the centroid of the projected geometry.
+ */
+ y?: ChannelValue;
+
/**
* The size of Point and MultiPoint geometries, defaulting to a constant 3
* pixels. If **r** is a number, it is interpreted as a constant radius in
diff --git a/src/marks/geo.js b/src/marks/geo.js
index e986539377..1c33f0a269 100644
--- a/src/marks/geo.js
+++ b/src/marks/geo.js
@@ -4,6 +4,7 @@ import {negative, positive} from "../defined.js";
import {Mark} from "../mark.js";
import {identity, maybeNumberChannel} from "../options.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
+import {centroid} from "../transforms/centroid.js";
import {withDefaultSort} from "./dot.js";
const defaults = {
@@ -22,8 +23,10 @@ export class Geo extends Mark {
super(
data,
{
- geometry: {value: options.geometry, scale: "projection"},
- r: {value: vr, scale: "r", filter: positive, optional: true}
+ x: {value: options.tip ? options.x : null, scale: "x", optional: true},
+ y: {value: options.tip ? options.y : null, scale: "y", optional: true},
+ r: {value: vr, scale: "r", filter: positive, optional: true},
+ geometry: {value: options.geometry, scale: "projection"}
},
withDefaultSort(options),
defaults
@@ -66,7 +69,7 @@ function scaleProjection({x: X, y: Y}) {
}
}
-export function geo(data, {geometry = identity, ...options} = {}) {
+export function geo(data, options = {}) {
switch (data?.type) {
case "FeatureCollection":
data = data.features;
@@ -85,7 +88,9 @@ export function geo(data, {geometry = identity, ...options} = {}) {
data = [data];
break;
}
- return new Geo(data, {geometry, ...options});
+ if (options.tip && options.x === undefined && options.y === undefined) options = centroid(options);
+ else if (options.geometry === undefined) options = {...options, geometry: identity};
+ return new Geo(data, options);
}
export function sphere({strokeWidth = 1.5, ...options} = {}) {
diff --git a/src/marks/tip.js b/src/marks/tip.js
index d741b8e103..5e8d1ed83f 100644
--- a/src/marks/tip.js
+++ b/src/marks/tip.js
@@ -344,6 +344,7 @@ function getSourceChannels(channels, scales) {
// Then fallback to all other (non-ignored) channels.
for (const key in channels) {
if (key in sources || key in format || ignoreChannels.has(key)) continue;
+ if ((key === "x" || key === "y") && channels.geometry) continue; // ignore x & y on geo
const source = getSource(channels, key);
if (source) sources[key] = source;
}
diff --git a/src/memoize.js b/src/memoize.js
index 9395e658be..eceeac7d07 100644
--- a/src/memoize.js
+++ b/src/memoize.js
@@ -1,7 +1,25 @@
+const unset = Symbol("unset");
+
export function memoize1(compute) {
+ return (compute.length === 1 ? memoize1Arg : memoize1Args)(compute);
+}
+
+function memoize1Arg(compute) {
+ let cacheValue;
+ let cacheKey = unset;
+ return (key) => {
+ if (!Object.is(cacheKey, key)) {
+ cacheKey = key;
+ cacheValue = compute(key);
+ }
+ return cacheValue;
+ };
+}
+
+function memoize1Args(compute) {
let cacheValue, cacheKeys;
return (...keys) => {
- if (cacheKeys?.length !== keys.length || cacheKeys.some((k, i) => k !== keys[i])) {
+ if (cacheKeys?.length !== keys.length || cacheKeys.some((k, i) => !Object.is(k, keys[i]))) {
cacheKeys = keys;
cacheValue = compute(...keys);
}
diff --git a/src/transforms/centroid.js b/src/transforms/centroid.js
index b832775164..a7d745e64f 100644
--- a/src/transforms/centroid.js
+++ b/src/transforms/centroid.js
@@ -1,32 +1,40 @@
import {geoCentroid as GeoCentroid, geoPath} from "d3";
+import {memoize1} from "../memoize.js";
import {identity, valueof} from "../options.js";
import {initializer} from "./basic.js";
export function centroid({geometry = identity, ...options} = {}) {
- // Suppress defaults for x and y since they will be computed by the initializer.
- return initializer({...options, x: null, y: null}, (data, facets, channels, scales, dimensions, {projection}) => {
- const G = valueof(data, geometry);
- const n = G.length;
- const X = new Float64Array(n);
- const Y = new Float64Array(n);
- const path = geoPath(projection);
- for (let i = 0; i < n; ++i) [X[i], Y[i]] = path.centroid(G[i]);
- return {
- data,
- facets,
- channels: {
- x: {value: X, scale: projection == null ? "x" : null, source: null},
- y: {value: Y, scale: projection == null ? "y" : null, source: null}
- }
- };
- });
+ const getG = memoize1((data) => valueof(data, geometry));
+ return initializer(
+ // Suppress defaults for x and y since they will be computed by the initializer.
+ // Propagate the (memoized) geometry channel in case it’s still needed.
+ {...options, x: null, y: null, geometry: {transform: getG}},
+ (data, facets, channels, scales, dimensions, {projection}) => {
+ const G = getG(data);
+ const n = G.length;
+ const X = new Float64Array(n);
+ const Y = new Float64Array(n);
+ const path = geoPath(projection);
+ for (let i = 0; i < n; ++i) [X[i], Y[i]] = path.centroid(G[i]);
+ return {
+ data,
+ facets,
+ channels: {
+ x: {value: X, scale: projection == null ? "x" : null, source: null},
+ y: {value: Y, scale: projection == null ? "y" : null, source: null}
+ }
+ };
+ }
+ );
}
export function geoCentroid({geometry = identity, ...options} = {}) {
- let C;
+ const getG = memoize1((data) => valueof(data, geometry));
+ const getC = memoize1((data) => valueof(getG(data), GeoCentroid));
return {
...options,
- x: {transform: (data) => Float64Array.from((C = valueof(valueof(data, geometry), GeoCentroid)), ([x]) => x)},
- y: {transform: () => Float64Array.from(C, ([, y]) => y)}
+ x: {transform: (data) => Float64Array.from(getC(data), ([x]) => x)},
+ y: {transform: (data) => Float64Array.from(getC(data), ([, y]) => y)},
+ geometry: {transform: getG}
};
}
diff --git a/test/data/README.md b/test/data/README.md
index 303710c6dc..91eb29abbe 100644
--- a/test/data/README.md
+++ b/test/data/README.md
@@ -115,6 +115,14 @@ CBO
https://www.cbo.gov/topics/budget/accuracy-projections
https://observablehq.com/@tophtucker/examples-of-bitemporal-charts
+## london.json
+giCentre, City University of London
+https://github.com/gicentre/data
+
+## london-car-access.csv
+Derived by Jo Wood from UK Census data
+https://github.com/observablehq/plot/pull/2086
+
## metros.csv
The New York Times
https://www.nytimes.com/2019/12/02/upshot/wealth-poverty-divide-american-cities.html
diff --git a/test/data/london-car-access.csv b/test/data/london-car-access.csv
new file mode 100644
index 0000000000..3abc79f002
--- /dev/null
+++ b/test/data/london-car-access.csv
@@ -0,0 +1,34 @@
+borough,y2001,y2011,y2021
+City of London,0.380,0.306,0.228
+Barking and Dagenham,0.621,0.604,0.652
+Barnet,0.733,0.713,0.701
+Bexley,0.763,0.763,0.776
+Brent,0.627,0.570,0.559
+Bromley,0.770,0.765,0.771
+Camden,0.444,0.389,0.364
+Croydon,0.702,0.665,0.664
+Ealing,0.683,0.647,0.632
+Enfield,0.715,0.675,0.690
+Greenwich,0.592,0.580,0.569
+Hammersmith and Fulham,0.514,0.448,0.425
+Haringey,0.535,0.482,0.473
+Harrow,0.773,0.765,0.753
+Havering,0.767,0.770,0.785
+Hillingdon,0.783,0.773,0.777
+Hounslow,0.714,0.684,0.672
+Islington,0.424,0.353,0.331
+Kensington and Chelsea,0.496,0.440,0.417
+Kingston upon Thames,0.762,0.749,0.743
+Lambeth,0.491,0.422,0.420
+Lewisham,0.572,0.519,0.523
+Merton,0.699,0.674,0.670
+Newham,0.511,0.479,0.483
+Redbridge,0.738,0.721,0.725
+Richmond upon Thames,0.763,0.753,0.746
+Southwark,0.481,0.416,0.397
+Sutton,0.767,0.766,0.772
+Tower Hamlets,0.432,0.370,0.336
+Waltham Forest,0.610,0.581,0.579
+Wandsworth,0.593,0.547,0.521
+Westminster,0.436,0.371,0.338
+Hackney,0.440,0.354,0.351
diff --git a/test/data/london.json b/test/data/london.json
new file mode 100644
index 0000000000..7913a83231
--- /dev/null
+++ b/test/data/london.json
@@ -0,0 +1 @@
+{"type":"Topology","bbox":[-0.5103750689005356,51.28676016315083,0.3340155643740317,51.69187411690989],"transform":{"scale":[0.00008444750807826456,0.00004051544692059764],"translate":[-0.5103750689005356,51.28676016315083]},"objects":{"boroughs":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[0,1,2,3,4]],"id":"Kingston upon Thames","properties":{"GSSCode":"E09000021"}},{"type":"Polygon","arcs":[[5,6,7,8,9]],"id":"Croydon","properties":{"GSSCode":"E09000008"}},{"type":"Polygon","arcs":[[-9,10,11,12,13,14,15]],"id":"Bromley","properties":{"GSSCode":"E09000006"}},{"type":"Polygon","arcs":[[16,17,18,19,20,21]],"id":"Hounslow","properties":{"GSSCode":"E09000018"}},{"type":"Polygon","arcs":[[22,23,24,-21,25]],"id":"Ealing","properties":{"GSSCode":"E09000009"}},{"type":"Polygon","arcs":[[26,27,28]],"id":"Havering","properties":{"GSSCode":"E09000016"}},{"type":"Polygon","arcs":[[29,-26,-20,30]],"id":"Hillingdon","properties":{"GSSCode":"E09000017"}},{"type":"Polygon","arcs":[[31,32,33,-23,-30]],"id":"Harrow","properties":{"GSSCode":"E09000015"}},{"type":"Polygon","arcs":[[34,35,-24,-34,36,37,38]],"id":"Brent","properties":{"GSSCode":"E09000005"}},{"type":"Polygon","arcs":[[39,40,41,-37,-33,42]],"id":"Barnet","properties":{"GSSCode":"E09000003"}},{"type":"Polygon","arcs":[[43,44,-11,-8,45,46]],"id":"Lambeth","properties":{"GSSCode":"E09000022"}},{"type":"Polygon","arcs":[[47,48,-12,-45]],"id":"Southwark","properties":{"GSSCode":"E09000028"}},{"type":"Polygon","arcs":[[49,50,51,52,-13,-49]],"id":"Lewisham","properties":{"GSSCode":"E09000023"}},{"type":"MultiPolygon","arcs":[[[53,-51]],[[54,-14,-53,55]]],"id":"Greenwich","properties":{"GSSCode":"E09000011"}},{"type":"Polygon","arcs":[[56,-15,-55]],"id":"Bexley","properties":{"GSSCode":"E09000004"}},{"type":"Polygon","arcs":[[57,58,59,60,-40,61]],"id":"Enfield","properties":{"GSSCode":"E09000010"}},{"type":"Polygon","arcs":[[62,63,64,-58,65,66]],"id":"Waltham Forest","properties":{"GSSCode":"E09000031"}},{"type":"Polygon","arcs":[[67,68,-67,69,-28,70,71]],"id":"Redbridge","properties":{"GSSCode":"E09000026"}},{"type":"Polygon","arcs":[[72,-4,73,-6]],"id":"Sutton","properties":{"GSSCode":"E09000029"}},{"type":"Polygon","arcs":[[74,75,-1,76,-18]],"id":"Richmond upon Thames","properties":{"GSSCode":"E09000027"}},{"type":"Polygon","arcs":[[-74,-3,77,-46,-7]],"id":"Merton","properties":{"GSSCode":"E09000024"}},{"type":"Polygon","arcs":[[78,-47,-78,-2,-76]],"id":"Wandsworth","properties":{"GSSCode":"E09000032"}},{"type":"Polygon","arcs":[[-22,-25,-36,79,80]],"id":"Hammersmith and Fulham","properties":{"GSSCode":"E09000013"}},{"type":"Polygon","arcs":[[-80,-35,81,82]],"id":"Kensington and Chelsea","properties":{"GSSCode":"E09000020"}},{"type":"Polygon","arcs":[[-82,-39,83,84,85]],"id":"Westminster","properties":{"GSSCode":"E09000033"}},{"type":"Polygon","arcs":[[86,87,-84,-38,-42,88]],"id":"Camden","properties":{"GSSCode":"E09000007"}},{"type":"Polygon","arcs":[[89,90,91,92,93,94]],"id":"Tower Hamlets","properties":{"GSSCode":"E09000030"}},{"type":"Polygon","arcs":[[95,96,97,-87]],"id":"Islington","properties":{"GSSCode":"E09000019"}},{"type":"Polygon","arcs":[[-64,98,-93,99,-97,100]],"id":"Hackney","properties":{"GSSCode":"E09000012"}},{"type":"Polygon","arcs":[[-96,-89,-41,-61,59,-59,-65,-101]],"id":"Haringey","properties":{"GSSCode":"E09000014"}},{"type":"MultiPolygon","arcs":[[[101,102]],[[-94,-99,-63,-69,103,104]]],"id":"Newham","properties":{"GSSCode":"E09000025"}},{"type":"MultiPolygon","arcs":[[[-104,-68,105,-103,106]],[[-71,-27,107]]],"id":"Barking and Dagenham","properties":{"GSSCode":"E09000002"}},{"type":"Polygon","arcs":[[-85,-88,-98,-100,-92,90,-90,108]],"id":"City of London","properties":{"GSSCode":"E09000001"}}]}},"arcs":[[[2281,2639],[96,156],[14,361],[29,169],[-30,91],[-70,76],[45,98],[50,-51],[58,10],[96,-100],[33,62],[48,-218],[73,41],[112,190],[200,191]],[[3035,3715],[35,-116]],[[3070,3599],[-37,-83],[22,-72],[30,-242],[61,-299],[-32,-168],[68,-91],[23,-113]],[[3205,2531],[12,-79],[-75,-150]],[[3142,2302],[-101,-31],[-88,20],[-44,-98],[-86,-74],[-193,-260],[38,-21],[-72,-115],[-19,-96],[-87,-128],[-47,-110],[-19,-188],[-161,-188],[-107,-33],[-29,61],[31,373],[-28,108],[29,92],[69,100],[47,180],[3,168],[43,107],[67,20],[14,88],[-40,177],[-81,185]],[[4190,858],[130,49],[13,73],[-58,57],[63,47],[-42,200],[35,82],[202,132],[9,-64],[117,22],[-58,329],[6,140],[-80,389],[-17,165],[-58,91]],[[4452,2570],[117,161],[-79,163],[-15,108],[56,97]],[[4531,3099],[33,24],[65,141],[80,105],[223,-11],[88,-87],[94,14]],[[5114,3285],[-23,-106],[91,-71],[-8,-76],[78,-169],[88,-86],[76,54],[28,-47],[-33,-108],[132,-145],[68,-21],[-16,-129],[11,-153],[80,5],[40,53],[38,-177],[6,-128],[88,-201],[86,-80],[38,-80],[89,-574]],[[6071,1046],[-131,118],[-66,-102],[-74,182],[-64,35],[-81,-28],[-60,31],[-49,-121],[-98,-29],[29,-182],[-41,-69],[-152,-94],[-70,6],[-12,58],[-93,-36],[-70,-97],[34,-128],[-81,-85],[-28,-142],[-38,-52],[-198,-175],[-42,4],[-37,-132],[-77,-8],[-69,144],[-12,71],[-74,131],[-39,-30],[-174,42],[-28,82],[40,142],[-90,229],[64,47]],[[5114,3285],[2,19]],[[5116,3304],[52,136]],[[5168,3440],[131,-22],[72,-56],[155,-2],[74,52],[86,17],[110,-117],[-19,-84],[54,-61],[88,-38],[115,58],[30,66],[46,-3],[29,114],[72,77],[82,16],[52,53],[127,-163],[30,56],[-12,215],[-47,-41],[-99,99],[-7,63],[48,13],[6,72]],[[6391,3824],[12,65],[111,-82],[224,-405],[56,-8],[142,191]],[[6936,3585],[86,-44],[88,-170],[201,-224],[77,-29],[184,45],[235,-159]],[[7807,3004],[64,-87],[56,-228],[39,-79],[-173,7],[25,-47],[1,-269],[39,-48],[-24,-145],[-73,-230],[10,-114],[-39,-185],[-67,-162],[-140,42],[-77,-41],[-23,-68],[58,-190],[-87,-157],[-112,-1],[-126,-174],[-108,-106],[-26,-256],[42,-136],[57,-81],[-70,-97],[-317,-88],[-29,75],[-65,39],[-97,-32],[23,146],[-22,77],[-113,143],[-98,-139],[-65,-251],[-49,2],[-78,470],[23,131],[-45,-12],[-50,333]],[[3148,4984],[-45,-44],[-35,-106],[0,-108],[-36,-124],[-69,-47],[-108,57],[-77,206],[-69,106],[-132,30],[-79,-61],[-16,-78],[-73,-138],[-163,-127],[-12,-141]],[[2234,4409],[-59,-90],[13,-93],[-69,-17],[-66,-66],[-37,114],[-93,-60],[-100,40],[-197,-23],[-127,-197],[-47,-2],[18,-67],[99,-106],[79,21],[52,-40],[-131,-120],[21,-31],[-85,-84],[-57,-105],[47,-128],[-16,-48],[-70,39]],[[1409,3346],[-98,30],[-68,-26],[-100,187],[-87,52],[-99,-83],[-124,45],[3,98],[-94,10],[17,123],[-121,-46],[-14,274],[-45,-6],[33,181]],[[612,4185],[174,-75],[30,87],[57,22],[36,100],[133,117],[133,84],[-56,102],[-11,169],[50,191],[20,227],[47,47]],[[1225,5256],[235,-124],[109,47],[71,-151],[103,133],[148,80],[101,-76],[144,-23],[78,20],[50,-41],[40,40],[46,-96],[77,63],[-5,41],[98,-32],[-37,110],[79,55],[98,-20],[59,51],[119,-207],[181,12],[28,160]],[[3047,5298],[45,-110],[57,24],[-1,-228]],[[1570,6620],[23,-47],[158,73],[1,31],[176,59],[59,-54],[83,-22]],[[2070,6660],[90,-32],[180,-148],[66,-100],[10,-282],[43,-45],[82,82],[83,19],[75,64],[47,-82],[-9,-57],[-57,11],[-63,-45],[104,-79],[166,50],[104,119],[136,-63]],[[3127,6072],[-74,-190],[44,-281],[26,-60],[11,-164],[-111,-8],[24,-71]],[[1225,5256],[38,84],[126,106],[34,156],[90,104],[79,192],[-3,76],[-441,144],[-73,100],[168,93],[120,2],[-41,132],[159,81],[89,94]],[[7923,5564],[80,401],[63,103],[50,145],[50,45],[10,85],[68,86],[52,134],[-69,123],[-11,172],[-119,12],[-136,-86],[-144,199],[-27,274],[41,73],[2,289],[-35,87]],[[7798,7706],[16,97],[-25,237],[-109,272]],[[7680,8312],[202,-4],[161,-48],[137,75],[170,31],[65,-19],[125,44],[136,87],[21,37],[182,-213],[151,-132],[66,-91],[77,-154],[-74,-33],[-33,-130],[173,-41],[-6,-101],[32,-191],[119,-227],[97,-352],[269,38],[26,-196],[85,-127],[33,8],[74,-176],[31,-134],[-148,0],[-247,-43],[-35,-85],[-303,-52],[-80,-26],[0,-180],[-20,-173],[-117,1],[-35,269],[-95,-109],[-25,-78],[38,-71],[-80,24],[47,-138],[9,-142],[-180,-33],[39,-179],[-55,-45],[-115,-23],[-72,-170],[-170,-43],[-90,-2],[-34,68],[-27,217],[-46,144],[-176,163],[-59,7]],[[1259,8057],[23,-156],[-3,-122],[69,-166],[12,-124],[40,-42],[7,-171],[163,-656]],[[612,4185],[-177,57],[-37,65],[-122,4],[-81,32],[-89,115],[-105,3],[60,163],[20,158],[57,140],[1,74],[118,168],[-13,74],[43,48],[35,155],[-77,104],[-32,140],[20,240],[20,59],[-77,225],[105,289],[110,129],[-2,124],[-44,17],[-42,146],[9,196],[-49,41],[-17,130],[-60,68],[-58,187],[-16,198],[54,61],[-34,160],[45,246],[-57,78],[31,111],[-32,50],[47,75],[57,-24],[35,-89],[141,-130],[89,-125],[139,-111],[98,70],[43,111],[88,5],[133,-69],[125,-18],[59,-62],[86,-16]],[[1259,8057],[203,59],[240,130],[82,109],[124,40],[111,95],[19,-31],[88,133],[139,91],[28,49],[145,-104]],[[2438,8628],[6,-120],[436,-767]],[[2880,7741],[-111,-96],[-164,-72],[76,-83],[18,-128],[-244,55],[-119,-74],[-84,-95],[-77,-41],[54,-225],[-90,-63],[18,-113],[-87,-146]],[[3486,5952],[-148,60]],[[3338,6012],[-70,57],[-108,-21],[-33,24]],[[2880,7741],[224,-396],[-73,-70],[36,-206],[-21,-16],[73,-96],[3,70],[62,31],[95,-18],[136,-223],[100,-192]],[[3515,6625],[261,-466]],[[3776,6159],[-60,-213],[-76,129],[-145,-37],[-9,-86]],[[3887,9425],[-45,-143],[369,-180],[36,-251],[45,-81],[33,14],[44,-103],[145,-153],[-74,-261],[-108,-153],[68,-131]],[[4400,7983],[-55,-33],[16,-137],[-28,-79],[-82,-66],[-23,129],[-41,56],[-58,-179],[50,-274],[-98,-83],[-66,-266]],[[4015,7051],[-37,-79],[-43,30],[-140,-136],[-10,-77],[-92,-76],[-20,-63],[-67,-22],[-49,36],[-42,-39]],[[2438,8628],[99,-22],[260,86],[15,78],[114,68],[70,-74],[75,188],[14,127],[137,90],[111,43],[29,-70],[73,82],[147,59],[-39,101],[38,-9],[54,87],[101,-45],[-21,-69],[66,-40],[39,99],[67,18]],[[4523,4894],[49,67],[37,177],[21,220],[41,87],[83,27]],[[4754,5472],[35,-159],[-58,-163],[71,-47],[20,-58],[-63,-131],[53,-122],[-50,-16],[98,-84],[-8,-64],[55,-108],[35,52],[35,-143],[-7,-72],[-61,-145],[-61,-80],[2,-94],[56,-89],[65,-254],[32,-182],[113,-209]],[[4531,3099],[-75,-33],[-102,58],[-64,-11],[91,157]],[[4381,3270],[30,46],[-6,233],[37,-5],[-52,123],[44,165],[-91,-4],[-23,87],[38,139],[-64,33],[-36,264],[2,113],[177,126],[86,304]],[[4754,5472],[158,-3],[172,-60],[204,-128],[77,3],[81,54],[72,96],[104,-1],[38,-66],[0,-275]],[[5660,5092],[-97,-7],[-23,-63],[-112,-27],[-28,-260],[20,-15],[16,-262],[59,-73],[2,-184],[54,-49],[-54,-126],[-92,-29],[-43,40],[-73,-143],[2,-174],[-29,-76],[-72,-47],[-36,-91],[14,-66]],[[5660,5092],[89,-186]],[[5749,4906],[-18,-99],[39,-47],[71,20]],[[5841,4780],[-67,-125]],[[5774,4655],[43,-134],[71,-25],[-24,70],[83,25],[97,-23],[177,31],[16,-101],[-86,-77],[29,-229],[71,-38],[52,-151],[-5,-153],[93,-26]],[[5749,4906],[72,-50],[20,-76]],[[7467,5546],[12,-497],[12,-234],[23,-124],[-68,52],[-76,-144],[-45,75],[-73,-41],[-45,25],[-159,-167],[-30,-51],[7,-168],[-50,-14],[83,-65],[27,-248],[-7,-83],[-77,-70],[-65,-207]],[[5774,4655],[91,155],[-32,40],[125,13],[87,76],[30,122],[-35,85],[-12,200],[77,23],[116,-199],[105,-56],[194,37],[263,-24],[168,44],[68,67],[114,247],[176,10],[158,51]],[[7467,5546],[57,39],[86,-18],[147,-120],[183,-46],[78,-55],[63,-169],[6,-191],[65,-130],[73,-65],[119,-39],[211,82],[66,-54],[-17,-75],[-59,-52],[-37,-109],[34,-59],[-8,-144],[-93,-107],[4,-107],[-116,-56],[-64,-74],[-39,-100],[-99,-65],[-38,25],[-72,-197],[-32,-151],[-96,48],[-28,-185],[-28,-73],[-23,-197],[54,-29],[-57,-69]],[[5898,8872],[-37,-270],[-117,-319],[-88,-149],[-34,-106],[19,-64],[-88,-94]],[[5553,7870],[-168,81],[-136,1]],[[5249,7952],[0,0]],[[5249,7952],[-222,-12],[-115,20],[-135,-24],[-196,10],[-136,62],[-45,-25]],[[3887,9425],[76,112],[38,-2],[105,230],[1,141],[162,-64],[180,81],[153,-6],[112,61],[96,19],[111,-42],[106,2],[81,-31],[150,-125],[63,-24],[113,17],[282,-33],[196,-34],[9,-110],[-14,-308],[31,-91],[-35,-189],[-5,-157]],[[6285,6652],[-41,-76],[-28,44],[-60,-92],[-76,21],[-139,-54],[-23,67],[-78,-26]],[[5840,6536],[-15,89],[-129,152],[-47,-18],[-166,54],[7,41],[-136,194],[-35,135]],[[5319,7183],[-13,43],[120,209],[-16,47],[34,164],[109,224]],[[5898,8872],[58,-1],[-11,-62],[93,-3],[16,-43],[204,-38],[54,21],[35,-155],[-45,-148]],[[6302,8443],[-35,-138],[-52,-107],[-69,4],[90,-55],[61,-141],[-100,-298],[-10,-170],[15,-207],[48,-156],[-13,-112],[32,-146],[-54,-35],[-3,-105],[73,-125]],[[6865,6352],[-12,7]],[[6853,6359],[-6,214],[-50,105],[-41,-22],[-68,80],[-48,107],[-100,-63],[50,-163],[-132,9],[-114,-28],[-59,54]],[[6302,8443],[86,-92],[140,-233],[146,68],[-53,-119],[168,-165],[117,-55],[71,57],[98,-62],[65,42],[26,113],[-30,69],[69,46],[25,-67],[112,31],[84,73],[22,-38],[66,52],[59,98],[107,51]],[[7798,7706],[-19,11],[-201,-231],[17,-63],[-55,-19],[17,-126],[56,-8],[-40,-123],[29,-111],[-27,-132],[-122,-84],[-22,-153],[-72,-108],[-208,-164],[-15,68],[-166,-107],[-101,-5]],[[6869,6351],[-4,1]],[[4190,858],[-91,112],[13,104],[-116,-6],[-15,79],[-157,184],[-117,72],[-67,-93],[-119,-106],[-13,-128],[-81,-12],[-105,164],[149,170],[-56,201],[-18,170],[-40,103],[-121,106],[-99,-1],[11,89],[-6,236]],[[3205,2531],[60,30],[11,-73],[71,-89],[114,-94],[98,200],[110,-83],[78,74],[66,-61],[113,116],[47,82],[100,-92],[13,-43],[248,67],[29,-33],[89,38]],[[2234,4409],[85,-133],[94,-82],[33,-87],[-65,-116],[-106,-50],[-101,-16],[-58,-164],[64,-184],[-47,235],[96,100],[100,26],[94,62],[35,61],[-21,118],[-174,199],[7,148],[130,107],[120,220],[83,71],[68,-4],[93,-126],[44,-156],[64,-85],[90,-23],[63,30],[59,102],[19,205],[86,109],[122,-19],[32,-61],[21,-239],[34,-97]],[[3398,4560],[-114,15],[-3,-201],[-115,30],[-117,1],[-61,-122],[-13,-132],[140,-142],[67,-149],[-147,-145]],[[2281,2639],[-118,-47],[-160,286],[-31,15],[-189,199],[-79,-22],[-79,-63],[-75,-19],[-51,57],[-70,14],[-7,90],[49,-2],[-18,125],[-44,74]],[[3070,3599],[234,137],[84,18],[101,-27],[122,25],[182,66],[-12,-207],[63,3],[14,-137],[31,-72],[241,-120],[20,69],[183,-121],[48,37]],[[3398,4560],[119,-140],[159,-76],[136,24],[53,41],[65,133],[45,190],[81,64],[220,64],[141,-23],[106,57]],[[3338,6012],[21,-206],[-19,-22],[68,-135],[39,-150],[48,-47],[-33,-37],[46,-133],[132,-184],[60,-135],[41,-33],[125,-224]],[[3866,4706],[53,5],[-40,-199],[-42,-77],[-87,-40],[-103,12],[-160,94],[-72,117],[-29,237],[-58,138],[-45,31],[-135,-40]],[[3486,5952],[112,-45],[70,-131],[-37,-3],[54,-151],[49,12],[34,-127],[50,7],[44,-207],[46,-8],[9,-91],[167,23],[83,88],[43,-254],[-12,-58],[69,-102]],[[4267,4905],[-305,-95],[-42,-90],[-54,-14]],[[3776,6159],[32,-44],[102,125],[126,-24],[54,-70],[145,43],[66,-308],[51,-28],[69,-124],[73,-44],[39,-110],[55,62],[134,5]],[[4722,5642],[0,-113]],[[4722,5529],[-99,-49],[-30,-74],[-33,-345],[-75,-141],[-98,-32],[-120,17]],[[4357,6969],[16,-212],[46,-127],[71,-92],[51,-115],[51,-231],[0,-170],[78,-22],[43,-152],[63,-37],[20,-90]],[[4796,5721],[-100,-7],[26,-72]],[[4015,7051],[47,13],[201,-34],[94,-61]],[[5104,5457],[7,24]],[[5111,5481],[0,0]],[[5111,5481],[71,36],[-14,103],[-65,108],[11,66]],[[5114,5794],[28,16],[-13,127],[36,70],[93,27],[79,71],[116,57],[62,-13],[139,216],[46,-58],[147,26]],[[5847,6333],[-55,-153],[52,-148],[70,-43],[41,-79],[-4,-79]],[[5951,5831],[-16,-78],[58,-85],[19,34],[75,-80],[-27,-87],[87,-91],[-60,26],[-64,-24],[-57,-84],[-15,-79],[54,-214],[-5,-78],[-58,-60],[-93,-10],[-59,33],[-76,170],[0,264],[-26,59],[-127,59],[-84,-29],[-100,-121],[-77,-16],[-196,117]],[[4357,6969],[112,98],[159,60],[53,-122],[127,-143]],[[4808,6862],[68,-97],[82,-15],[15,-150],[127,-58],[34,-141],[-83,12],[-33,-230],[-123,-106],[19,-93],[123,-74],[-2,-145]],[[5035,5765],[-151,63],[3,-53],[-91,-54]],[[5840,6536],[-21,-27],[28,-176]],[[5114,5794],[-79,-29]],[[4808,6862],[98,145],[-20,73],[200,49],[55,-16],[178,70]],[[6938,5999],[-1,1]],[[6937,6000],[1,-1]],[[6853,6359],[-14,-125],[71,-246]],[[6910,5988],[63,-13],[80,-80],[80,1],[46,-252],[-66,-18],[-82,-78],[-115,-275],[-158,-50],[-192,39],[-210,-33],[-102,48],[-133,238],[9,102],[-115,95],[-29,-30],[-44,76],[9,73]],[[6865,6352],[-9,-83],[82,-270]],[[6937,6000],[-27,-12]],[[7923,5564],[-74,10],[-105,63],[-90,84],[-105,22],[-94,-32],[-148,-104],[-87,22],[-45,53],[-1,88],[-44,153],[-73,-16],[-107,92],[-90,300],[9,52]],[[5104,5457],[-241,72],[-141,0]]]}
diff --git a/test/marks/memoize-test.js b/test/marks/memoize-test.js
index c41f660642..4ef82acc70 100644
--- a/test/marks/memoize-test.js
+++ b/test/marks/memoize-test.js
@@ -1,71 +1,90 @@
import {memoize1} from "../../src/memoize.js";
import assert from "assert";
-it("memoize1(compute) returns the cached value with repeated calls", () => {
- let index = 0;
- const m = memoize1(() => ++index);
- assert.strictEqual(m(), 1);
- assert.strictEqual(m(), 1);
- assert.strictEqual(m(), 1);
- assert.strictEqual(m(0), 2);
- assert.strictEqual(m(0), 2);
- assert.strictEqual(m(0), 2);
-});
+describe("memoize1(compute)", () => {
+ it("returns the cached value with repeated calls", () => {
+ let index = 0;
+ const m = memoize1(() => ++index);
+ assert.strictEqual(m(), 1);
+ assert.strictEqual(m(), 1);
+ assert.strictEqual(m(), 1);
+ assert.strictEqual(m(0), 2);
+ assert.strictEqual(m(0), 2);
+ assert.strictEqual(m(0), 2);
+ });
-it("memoize1(compute) computes a new value with a different argument value", () => {
- let index = 0;
- const m = memoize1(() => ++index);
- assert.strictEqual(m(), 1);
- assert.strictEqual(m(0), 2);
- assert.strictEqual(m(undefined), 3);
- assert.strictEqual(m(null), 4);
- assert.strictEqual(m(0), 5);
- assert.strictEqual(m(0, 1), 6);
- assert.strictEqual(m(0, 2), 7);
-});
+ it("works with a single-argument function", () => {
+ let index = 0;
+ const m = memoize1((arg) => [arg, ++index]);
+ assert.deepStrictEqual(m(), [undefined, 1]);
+ assert.deepStrictEqual(m(), [undefined, 1]);
+ assert.deepStrictEqual(m(undefined), [undefined, 1]);
+ assert.deepStrictEqual(m(undefined), [undefined, 1]);
+ assert.deepStrictEqual(m(null), [null, 2]);
+ assert.deepStrictEqual(m(null), [null, 2]);
+ assert.deepStrictEqual(m(undefined), [undefined, 3]);
+ assert.deepStrictEqual(m(undefined), [undefined, 3]);
+ assert.deepStrictEqual(m(NaN), [NaN, 4]);
+ assert.deepStrictEqual(m(NaN), [NaN, 4]);
+ });
-it("memoize1(compute) computes a new value with different number of arguments", () => {
- let index = 0;
- const m = memoize1(() => ++index);
- assert.strictEqual(m(0), 1);
- assert.strictEqual(m(0, 0), 2);
- assert.strictEqual(m(0, 0, 0), 3);
- assert.strictEqual(m(0, 0), 4);
- assert.strictEqual(m(0), 5);
-});
+ it("computes a new value with a different argument value", () => {
+ let index = 0;
+ const m = memoize1(() => ++index);
+ assert.strictEqual(m(), 1);
+ assert.strictEqual(m(0), 2);
+ assert.strictEqual(m(undefined), 3);
+ assert.strictEqual(m(null), 4);
+ assert.strictEqual(m(0), 5);
+ assert.strictEqual(m(0, 1), 6);
+ assert.strictEqual(m(0, 2), 7);
+ });
-it("memoize1(compute) only caches a single value", () => {
- let index = 0;
- const m = memoize1(() => ++index);
- assert.strictEqual(m(0), 1);
- assert.strictEqual(m(1), 2);
- assert.strictEqual(m(1), 2);
- assert.strictEqual(m(0), 3);
- assert.strictEqual(m(0), 3);
- assert.strictEqual(m(1), 4);
- assert.strictEqual(m(1), 4);
-});
+ it("computes a new value with different number of arguments", () => {
+ let index = 0;
+ const m = memoize1(() => ++index);
+ assert.strictEqual(m(0), 1);
+ assert.strictEqual(m(0, 0), 2);
+ assert.strictEqual(m(0, 0, 0), 3);
+ assert.strictEqual(m(0, 0), 4);
+ assert.strictEqual(m(0), 5);
+ });
-it("memoize1(compute) determines equality with strict equals", () => {
- let index = 0;
- const m = memoize1(() => ++index);
- assert.strictEqual(m([0]), 1);
- assert.strictEqual(m([1]), 2);
- assert.strictEqual(m([1]), 3);
- assert.strictEqual(m([0]), 4);
- assert.strictEqual(m([0]), 5);
- assert.strictEqual(m([1]), 6);
- assert.strictEqual(m([1]), 7);
-});
+ it("only caches a single value", () => {
+ let index = 0;
+ const m = memoize1(() => ++index);
+ assert.strictEqual(m(0), 1);
+ assert.strictEqual(m(1), 2);
+ assert.strictEqual(m(1), 2);
+ assert.strictEqual(m(0), 3);
+ assert.strictEqual(m(0), 3);
+ assert.strictEqual(m(1), 4);
+ assert.strictEqual(m(1), 4);
+ });
+
+ it("determines equality with strict equals", () => {
+ let index = 0;
+ const m = memoize1(() => ++index);
+ assert.strictEqual(m([0]), 1);
+ assert.strictEqual(m([1]), 2);
+ assert.strictEqual(m([1]), 3);
+ assert.strictEqual(m([0]), 4);
+ assert.strictEqual(m([0]), 5);
+ assert.strictEqual(m([1]), 6);
+ assert.strictEqual(m([1]), 7);
+ });
-it("memoize1(compute) passes the specified arguments to compute", () => {
- let index = 0;
- const m = memoize1((...args) => [...args, ++index]);
- assert.deepStrictEqual(m(0), [0, 1]);
- assert.deepStrictEqual(m(1), [1, 2]);
- assert.deepStrictEqual(m(1), [1, 2]);
- assert.deepStrictEqual(m(0, 1), [0, 1, 3]);
- assert.deepStrictEqual(m(0, 1), [0, 1, 3]);
- assert.deepStrictEqual(m(1, 0), [1, 0, 4]);
- assert.deepStrictEqual(m(1, 0), [1, 0, 4]);
+ it("passes the specified arguments to compute", () => {
+ let index = 0;
+ const m = memoize1((...args) => [...args, ++index]);
+ assert.deepStrictEqual(m(0), [0, 1]);
+ assert.deepStrictEqual(m(1), [1, 2]);
+ assert.deepStrictEqual(m(1), [1, 2]);
+ assert.deepStrictEqual(m(0, 1), [0, 1, 3]);
+ assert.deepStrictEqual(m(0, 1), [0, 1, 3]);
+ assert.deepStrictEqual(m(1, 0), [1, 0, 4]);
+ assert.deepStrictEqual(m(1, 0), [1, 0, 4]);
+ assert.deepStrictEqual(m(1, NaN), [1, NaN, 5]);
+ assert.deepStrictEqual(m(1, NaN), [1, NaN, 5]);
+ });
});
diff --git a/test/output/geoText.svg b/test/output/geoText.svg
new file mode 100644
index 0000000000..bb2ecf855a
--- /dev/null
+++ b/test/output/geoText.svg
@@ -0,0 +1,86 @@
+
\ No newline at end of file
diff --git a/test/output/geoTip.svg b/test/output/geoTip.svg
new file mode 100644
index 0000000000..1bbaa91313
--- /dev/null
+++ b/test/output/geoTip.svg
@@ -0,0 +1,142 @@
+
\ No newline at end of file
diff --git a/test/output/geoTip2.svg b/test/output/geoTip2.svg
new file mode 100644
index 0000000000..b3fa342a4b
--- /dev/null
+++ b/test/output/geoTip2.svg
@@ -0,0 +1,131 @@
+
\ No newline at end of file
diff --git a/test/output/geoTipCentroid.svg b/test/output/geoTipCentroid.svg
new file mode 100644
index 0000000000..1bbaa91313
--- /dev/null
+++ b/test/output/geoTipCentroid.svg
@@ -0,0 +1,142 @@
+
\ No newline at end of file
diff --git a/test/output/geoTipGeoCentroid.svg b/test/output/geoTipGeoCentroid.svg
new file mode 100644
index 0000000000..1bbaa91313
--- /dev/null
+++ b/test/output/geoTipGeoCentroid.svg
@@ -0,0 +1,142 @@
+
\ No newline at end of file
diff --git a/test/output/geoTipXY.svg b/test/output/geoTipXY.svg
new file mode 100644
index 0000000000..1bbaa91313
--- /dev/null
+++ b/test/output/geoTipXY.svg
@@ -0,0 +1,142 @@
+
\ No newline at end of file
diff --git a/test/plots/geo-tip.ts b/test/plots/geo-tip.ts
new file mode 100644
index 0000000000..3079418444
--- /dev/null
+++ b/test/plots/geo-tip.ts
@@ -0,0 +1,130 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+import {feature} from "topojson-client";
+
+export async function geoText() {
+ const london = feature(await d3.json("data/london.json"), "boroughs");
+ return Plot.plot({
+ projection: {type: "transverse-mercator", rotate: [2, 0, 0], domain: london},
+ marks: [
+ Plot.geo(london),
+ Plot.text(london.features, Plot.centroid({text: "id", stroke: "var(--plot-background)", fill: "currentColor"}))
+ ]
+ });
+}
+
+/** The geo mark with the tip option. */
+export async function geoTip() {
+ const [london, boroughs] = await getLondonBoroughs();
+ const access = await getLondonAccess();
+ return Plot.plot({
+ width: 900,
+ projection: {type: "transverse-mercator", rotate: [2, 0, 0], domain: london},
+ color: {scheme: "RdYlBu", pivot: 0.5},
+ marks: [
+ Plot.geo(access, {
+ fx: "year",
+ geometry: (d) => boroughs.get(d.borough),
+ fill: "access",
+ stroke: "var(--plot-background)",
+ strokeWidth: 0.75,
+ channels: {borough: "borough"},
+ tip: true
+ })
+ ]
+ });
+}
+
+/** The geo mark with the tip option and the centroid transform. */
+export async function geoTipCentroid() {
+ const [london, boroughs] = await getLondonBoroughs();
+ const access = await getLondonAccess();
+ return Plot.plot({
+ width: 900,
+ projection: {type: "transverse-mercator", rotate: [2, 0, 0], domain: london},
+ color: {scheme: "RdYlBu", pivot: 0.5},
+ marks: [
+ Plot.geo(
+ access,
+ Plot.centroid({
+ fx: "year",
+ geometry: (d) => boroughs.get(d.borough),
+ fill: "access",
+ stroke: "var(--plot-background)",
+ strokeWidth: 0.75,
+ channels: {borough: "borough"},
+ tip: true
+ })
+ )
+ ]
+ });
+}
+
+/** The geo mark with the tip option and the geoCentroid transform. */
+export async function geoTipGeoCentroid() {
+ const [london, boroughs] = await getLondonBoroughs();
+ const access = await getLondonAccess();
+ return Plot.plot({
+ width: 900,
+ projection: {type: "transverse-mercator", rotate: [2, 0, 0], domain: london},
+ color: {scheme: "RdYlBu", pivot: 0.5},
+ marks: [
+ Plot.geo(
+ access,
+ Plot.geoCentroid({
+ fx: "year",
+ geometry: (d) => boroughs.get(d.borough),
+ fill: "access",
+ stroke: "var(--plot-background)",
+ strokeWidth: 0.75,
+ channels: {borough: "borough"},
+ tip: true
+ })
+ )
+ ]
+ });
+}
+
+function getFirstPoint(feature) {
+ return feature.geometry.type === "Polygon"
+ ? feature.geometry.coordinates[0][0]
+ : feature.geometry.coordinates[0][0][0];
+}
+
+/** The geo mark with the tip option and x and y channels. */
+export async function geoTipXY() {
+ const [london, boroughs] = await getLondonBoroughs();
+ const access = await getLondonAccess();
+ return Plot.plot({
+ width: 900,
+ projection: {type: "transverse-mercator", rotate: [2, 0, 0], domain: london},
+ color: {scheme: "RdYlBu", pivot: 0.5},
+ marks: [
+ Plot.geo(access, {
+ x: (d) => getFirstPoint(boroughs.get(d.borough))[0],
+ y: (d) => getFirstPoint(boroughs.get(d.borough))[1],
+ fx: "year",
+ geometry: (d) => boroughs.get(d.borough),
+ fill: "access",
+ stroke: "var(--plot-background)",
+ strokeWidth: 0.75,
+ channels: {borough: "borough"},
+ tip: true
+ })
+ ]
+ });
+}
+
+async function getLondonBoroughs() {
+ const london = feature(await d3.json("data/london.json"), "boroughs");
+ const boroughs = new Map(london.features.map((d) => [d.id, d]));
+ return [london, boroughs];
+}
+
+async function getLondonAccess() {
+ return (await d3.csv("data/london-car-access.csv", d3.autoType)).flatMap(({borough, y2001, y2011, y2021}) => [
+ {borough, year: "2001", access: y2001},
+ {borough, year: "2011", access: y2011},
+ {borough, year: "2021", access: y2021}
+ ]);
+}
diff --git a/test/plots/index.ts b/test/plots/index.ts
index 907109e590..f1b253323e 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -103,6 +103,7 @@ export * from "./fruit-sales.js";
export * from "./function-contour.js";
export * from "./geo-line.js";
export * from "./geo-link.js";
+export * from "./geo-tip.js";
export * from "./gistemp-anomaly-moving.js";
export * from "./gistemp-anomaly-transform.js";
export * from "./gistemp-anomaly.js";