diff --git a/src/channel.js b/src/channel.js
index afcd3992e5..36cbf496eb 100644
--- a/src/channel.js
+++ b/src/channel.js
@@ -68,7 +68,7 @@ export function channelDomain(channels, facetChannels, data, options) {
let domain = rollup(
range(XV),
(I) => reducer.reduce(I, YV),
- (i) => XV[i]
+ (i) => XV[i % XV.length]
);
domain = sort(domain, reverse ? descendingGroup : ascendingGroup);
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
@@ -88,7 +88,7 @@ function findScaleChannel(channels, scale) {
function difference(channels, k1, k2) {
const X1 = values(channels, k1);
const X2 = values(channels, k2);
- return map(X2, (x2, i) => Math.abs(x2 - X1[i]), Float64Array);
+ return map(X2, (x2, i) => Math.abs(x2 - X1[i % X1.length]), Float64Array);
}
function values(channels, name, alias) {
diff --git a/src/facet.js b/src/facet.js
new file mode 100644
index 0000000000..00b858c745
--- /dev/null
+++ b/src/facet.js
@@ -0,0 +1,17 @@
+// Make sure the facets are exclusive, possibly by creating a rectangular index
+// of size m * n, where m is the number of facets, and n the data’s length
+export function facetExclusive(facets, n) {
+ const m = facets.length;
+ if (m === 1) return {facets, n};
+ const overlap = new Uint8Array(n);
+ let max = -Infinity;
+ for (const facet of facets) {
+ for (const i of facet) {
+ if (overlap[i]) return {facets: facets.map((f, i) => f.map((d) => d + i * n)), n: m * n};
+ overlap[i] = 1;
+ if (i > max) max = i;
+ }
+ }
+ // If the facets were already expanded, return a sufficient multiple of n
+ return {facets, n: n * (1 + Math.floor(max / n))};
+}
diff --git a/src/marks/area.js b/src/marks/area.js
index 1234b16e2c..0580e76c2e 100644
--- a/src/marks/area.js
+++ b/src/marks/area.js
@@ -61,10 +61,10 @@ export class Area extends Mark {
shapeArea()
.curve(this.curve)
.defined((i) => i >= 0)
- .x0((i) => X1[i])
- .y0((i) => Y1[i])
- .x1((i) => X2[i])
- .y1((i) => Y2[i])
+ .x0((i) => X1[i % X1.length])
+ .y0((i) => Y1[i % Y1.length])
+ .x1((i) => X2[i % X2.length])
+ .y1((i) => Y2[i % Y2.length])
)
)
.node();
diff --git a/src/marks/arrow.js b/src/marks/arrow.js
index 79fbefa392..9e24cb24f5 100644
--- a/src/marks/arrow.js
+++ b/src/marks/arrow.js
@@ -48,7 +48,7 @@ export class Arrow extends Mark {
render(index, scales, channels, dimensions, context) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, SW} = channels;
const {strokeWidth, bend, headAngle, headLength, insetStart, insetEnd} = this;
- const sw = SW ? (i) => SW[i] : constant(strokeWidth === undefined ? 1 : strokeWidth);
+ const sw = SW ? (i) => SW[i % SW.length] : constant(strokeWidth === undefined ? 1 : strokeWidth);
// When bending, the offset between the straight line between the two points
// and the outgoing tangent from the start point. (Also the negative
@@ -78,10 +78,10 @@ export class Arrow extends Mark {
.attr("d", (i) => {
// The start ⟨x1,y1⟩ and end ⟨x2,y2⟩ points may be inset, and the
// ending line angle may be altered for inset swoopy arrows.
- let x1 = X1[i],
- y1 = Y1[i],
- x2 = X2[i],
- y2 = Y2[i];
+ let x1 = X1[i % X1.length],
+ y1 = Y1[i % Y1.length],
+ x2 = X2[i % X2.length],
+ y2 = Y2[i % Y2.length];
const lineLength = Math.hypot(x2 - x1, y2 - y1);
if (lineLength <= insetStart + insetEnd) return null;
let lineAngle = Math.atan2(y2 - y1, x2 - x1);
diff --git a/src/marks/bar.js b/src/marks/bar.js
index 0a3874f981..b5ef27d21c 100644
--- a/src/marks/bar.js
+++ b/src/marks/bar.js
@@ -49,11 +49,11 @@ export class AbstractBar extends Mark {
}
_x(scales, {x: X}, {marginLeft}) {
const {insetLeft} = this;
- return X ? (i) => X[i] + insetLeft : marginLeft + insetLeft;
+ return X ? (i) => X[i % X.length] + insetLeft : marginLeft + insetLeft;
}
_y(scales, {y: Y}, {marginTop}) {
const {insetTop} = this;
- return Y ? (i) => Y[i] + insetTop : marginTop + insetTop;
+ return Y ? (i) => Y[i % Y.length] + insetTop : marginTop + insetTop;
}
_width({x}, {x: X}, {marginRight, marginLeft, width}) {
const {insetLeft, insetRight} = this;
@@ -90,13 +90,13 @@ export class BarX extends AbstractBar {
}
_x({x}, {x1: X1, x2: X2}, {marginLeft}) {
const {insetLeft} = this;
- return isCollapsed(x) ? marginLeft + insetLeft : (i) => Math.min(X1[i], X2[i]) + insetLeft;
+ return isCollapsed(x) ? marginLeft + insetLeft : (i) => Math.min(X1[i % X1.length], X2[i % X2.length]) + insetLeft;
}
_width({x}, {x1: X1, x2: X2}, {marginRight, marginLeft, width}) {
const {insetLeft, insetRight} = this;
return isCollapsed(x)
? width - marginRight - marginLeft - insetLeft - insetRight
- : (i) => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight);
+ : (i) => Math.max(0, Math.abs(X2[i % X2.length] - X1[i % X1.length]) - insetLeft - insetRight);
}
}
@@ -119,13 +119,13 @@ export class BarY extends AbstractBar {
}
_y({y}, {y1: Y1, y2: Y2}, {marginTop}) {
const {insetTop} = this;
- return isCollapsed(y) ? marginTop + insetTop : (i) => Math.min(Y1[i], Y2[i]) + insetTop;
+ return isCollapsed(y) ? marginTop + insetTop : (i) => Math.min(Y1[i % Y1.length], Y2[i % Y2.length]) + insetTop;
}
_height({y}, {y1: Y1, y2: Y2}, {marginTop, marginBottom, height}) {
const {insetTop, insetBottom} = this;
return isCollapsed(y)
? height - marginTop - marginBottom - insetTop - insetBottom
- : (i) => Math.max(0, Math.abs(Y2[i] - Y1[i]) - insetTop - insetBottom);
+ : (i) => Math.max(0, Math.abs(Y2[i % Y2.length] - Y1[i % Y1.length]) - insetTop - insetBottom);
}
}
diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js
index e80603db4f..148538509f 100644
--- a/src/marks/delaunay.js
+++ b/src/marks/delaunay.js
@@ -68,8 +68,8 @@ class DelaunayLink extends Mark {
const {x: X, y: Y, z: Z} = channels;
const {curve} = this;
const [cx, cy] = applyFrameAnchor(this, dimensions);
- const xi = X ? (i) => X[i] : constant(cx);
- const yi = Y ? (i) => Y[i] : constant(cy);
+ const xi = X ? (i) => X[i % X.length] : constant(cx);
+ const yi = Y ? (i) => Y[i % Y.length] : constant(cy);
const mark = this;
function links(index) {
@@ -113,8 +113,8 @@ class DelaunayLink extends Mark {
const p = path();
const c = curve(p);
c.lineStart();
- c.point(X1[i], Y1[i]);
- c.point(X2[i], Y2[i]);
+ c.point(X1[i % X1.length], Y1[i % Y1.length]);
+ c.point(X2[i % X2.length], Y2[i % Y2.length]);
c.lineEnd();
return p;
})
@@ -130,7 +130,7 @@ class DelaunayLink extends Mark {
? (g) =>
g
.selectAll()
- .data(group(index, (i) => Z[i]).values())
+ .data(group(index, (i) => Z[i % Z.length]).values())
.enter()
.append("g")
.each(links)
@@ -157,8 +157,8 @@ class AbstractDelaunayMark extends Mark {
render(index, scales, channels, dimensions, context) {
const {x: X, y: Y, z: Z} = channels;
const [cx, cy] = applyFrameAnchor(this, dimensions);
- const xi = X ? (i) => X[i] : constant(cx);
- const yi = Y ? (i) => Y[i] : constant(cy);
+ const xi = X ? (i) => X[i % X.length] : constant(cx);
+ const yi = Y ? (i) => Y[i % Y.length] : constant(cy);
const mark = this;
function mesh(index) {
@@ -179,7 +179,7 @@ class AbstractDelaunayMark extends Mark {
? (g) =>
g
.selectAll()
- .data(group(index, (i) => Z[i]).values())
+ .data(group(index, (i) => Z[i % Z.length]).values())
.enter()
.append("g")
.each(mesh)
@@ -225,8 +225,8 @@ class Voronoi extends Mark {
render(index, scales, channels, dimensions, context) {
const {x: X, y: Y, z: Z} = channels;
const [cx, cy] = applyFrameAnchor(this, dimensions);
- const xi = X ? (i) => X[i] : constant(cx);
- const yi = Y ? (i) => Y[i] : constant(cy);
+ const xi = X ? (i) => X[i % X.length] : constant(cx);
+ const yi = Y ? (i) => Y[i % Y.length] : constant(cy);
function cells(index) {
const delaunay = Delaunay.from(index, xi, yi);
@@ -249,7 +249,7 @@ class Voronoi extends Mark {
? (g) =>
g
.selectAll()
- .data(group(index, (i) => Z[i]).values())
+ .data(group(index, (i) => Z[i % Z.length]).values())
.enter()
.append("g")
.each(cells)
diff --git a/src/marks/density.js b/src/marks/density.js
index a8d9cf2d93..3e988ffae6 100644
--- a/src/marks/density.js
+++ b/src/marks/density.js
@@ -126,9 +126,9 @@ function densityInitializer(options, fillDensity, strokeDensity) {
const SD = strokeDensity && [];
const density = contourDensity()
- .x(X ? (i) => X[i] : cx)
- .y(Y ? (i) => Y[i] : cy)
- .weight(W ? (i) => W[i] : 1)
+ .x(X ? (i) => X[i % X.length] : cx)
+ .y(Y ? (i) => Y[i % Y.length] : cy)
+ .weight(W ? (i) => W[i % W.length] : 1)
.size([width, height])
.bandwidth(bandwidth);
diff --git a/src/marks/dot.js b/src/marks/dot.js
index 6b3e380cc5..03e8044bfa 100644
--- a/src/marks/dot.js
+++ b/src/marks/dot.js
@@ -77,32 +77,32 @@ export class Dot extends Mark {
circle
? (selection) => {
selection
- .attr("cx", X ? (i) => X[i] : cx)
- .attr("cy", Y ? (i) => Y[i] : cy)
- .attr("r", R ? (i) => R[i] : this.r);
+ .attr("cx", X ? (i) => X[i % X.length] : cx)
+ .attr("cy", Y ? (i) => Y[i % Y.length] : cy)
+ .attr("r", R ? (i) => R[i % R.length] : this.r);
}
: (selection) => {
const translate =
X && Y
- ? (i) => `translate(${X[i]},${Y[i]})`
+ ? (i) => `translate(${X[i % X.length]},${Y[i % Y.length]})`
: X
- ? (i) => `translate(${X[i]},${cy})`
+ ? (i) => `translate(${X[i % X.length]},${cy})`
: Y
- ? (i) => `translate(${cx},${Y[i]})`
+ ? (i) => `translate(${cx},${Y[i % Y.length]})`
: () => `translate(${cx},${cy})`;
selection
.attr(
"transform",
A
- ? (i) => `${translate(i)} rotate(${A[i]})`
+ ? (i) => `${translate(i)} rotate(${A[i % A.length]})`
: this.rotate
? (i) => `${translate(i)} rotate(${this.rotate})`
: translate
)
.attr("d", (i) => {
const p = path(),
- r = R ? R[i] : this.r;
- (S ? S[i] : this.symbol).draw(p, r * r * Math.PI);
+ r = R ? R[i % R.length] : this.r;
+ (S ? S[i % S.length] : this.symbol).draw(p, r * r * Math.PI);
return p;
});
}
diff --git a/src/marks/image.js b/src/marks/image.js
index efe203a40f..d98fc59628 100644
--- a/src/marks/image.js
+++ b/src/marks/image.js
@@ -81,26 +81,26 @@ export class Image extends Mark {
.attr(
"x",
W && X
- ? (i) => X[i] - W[i] / 2
+ ? (i) => X[i % X.length] - W[i % W.length] / 2
: W
- ? (i) => cx - W[i] / 2
+ ? (i) => cx - W[i % W.length] / 2
: X
- ? (i) => X[i] - this.width / 2
+ ? (i) => X[i % X.length] - this.width / 2
: cx - this.width / 2
)
.attr(
"y",
H && Y
- ? (i) => Y[i] - H[i] / 2
+ ? (i) => Y[i % Y.length] - H[i % H.length] / 2
: H
- ? (i) => cy - H[i] / 2
+ ? (i) => cy - H[i % H.length] / 2
: Y
- ? (i) => Y[i] - this.height / 2
+ ? (i) => Y[i % Y.length] - this.height / 2
: cy - this.height / 2
)
- .attr("width", W ? (i) => W[i] : this.width)
- .attr("height", H ? (i) => H[i] : this.height)
- .call(applyAttr, "href", S ? (i) => S[i] : this.src)
+ .attr("width", W ? (i) => W[i % W.length] : this.width)
+ .attr("height", H ? (i) => H[i % H.length] : this.height)
+ .call(applyAttr, "href", S ? (i) => S[i % S.length] : this.src)
.call(applyAttr, "preserveAspectRatio", this.preserveAspectRatio)
.call(applyAttr, "crossorigin", this.crossOrigin)
.call(applyChannelStyles, this, channels)
diff --git a/src/marks/line.js b/src/marks/line.js
index a4db931266..0f15fe773d 100644
--- a/src/marks/line.js
+++ b/src/marks/line.js
@@ -62,8 +62,8 @@ export class Line extends Mark {
shapeLine()
.curve(this.curve)
.defined((i) => i >= 0)
- .x((i) => X[i])
- .y((i) => Y[i])
+ .x((i) => X[i % X.length])
+ .y((i) => Y[i % Y.length])
)
)
.node();
diff --git a/src/marks/linearRegression.js b/src/marks/linearRegression.js
index 7cdd44902d..35a7f4d557 100644
--- a/src/marks/linearRegression.js
+++ b/src/marks/linearRegression.js
@@ -86,7 +86,7 @@ class LinearRegressionX extends LinearRegression {
}
_renderBand(I, X, Y) {
const {ci, precision} = this;
- const [y1, y2] = extent(I, (i) => Y[i]);
+ const [y1, y2] = extent(I, (i) => Y[i % Y.length]);
const f = linearRegressionF(I, Y, X);
const g = confidenceIntervalF(I, Y, X, (1 - ci) / 2, f);
return shapeArea()
@@ -95,7 +95,7 @@ class LinearRegressionX extends LinearRegression {
.x1((y) => g(y, +1))(range(y1, y2 - precision / 2, precision).concat(y2));
}
_renderLine(I, X, Y) {
- const [y1, y2] = extent(I, (i) => Y[i]);
+ const [y1, y2] = extent(I, (i) => Y[i % Y.length]);
const f = linearRegressionF(I, Y, X);
return `M${f(y1)},${y1}L${f(y2)},${y2}`;
}
@@ -107,7 +107,7 @@ class LinearRegressionY extends LinearRegression {
}
_renderBand(I, X, Y) {
const {ci, precision} = this;
- const [x1, x2] = extent(I, (i) => X[i]);
+ const [x1, x2] = extent(I, (i) => X[i % X.length]);
const f = linearRegressionF(I, X, Y);
const g = confidenceIntervalF(I, X, Y, (1 - ci) / 2, f);
return shapeArea()
@@ -116,7 +116,7 @@ class LinearRegressionY extends LinearRegression {
.y1((x) => g(x, +1))(range(x1, x2 - precision / 2, precision).concat(x2));
}
_renderLine(I, X, Y) {
- const [x1, x2] = extent(I, (i) => X[i]);
+ const [x1, x2] = extent(I, (i) => X[i % X.length]);
const f = linearRegressionF(I, X, Y);
return `M${x1},${f(x1)}L${x2},${f(x2)}`;
}
@@ -166,8 +166,8 @@ function linearRegressionF(I, X, Y) {
sumXY = 0,
sumX2 = 0;
for (const i of I) {
- const xi = X[i];
- const yi = Y[i];
+ const xi = X[i % X.length];
+ const yi = Y[i % Y.length];
sumX += xi;
sumY += yi;
sumXY += xi * yi;
@@ -180,12 +180,12 @@ function linearRegressionF(I, X, Y) {
}
function confidenceIntervalF(I, X, Y, p, f) {
- const mean = sum(I, (i) => X[i]) / I.length;
+ const mean = sum(I, (i) => X[i % X.length]) / I.length;
let a = 0,
b = 0;
for (const i of I) {
- a += (X[i] - mean) ** 2;
- b += (Y[i] - f(X[i])) ** 2;
+ a += (X[i % X.length] - mean) ** 2;
+ b += (Y[i % Y.length] - f(X[i % X.length])) ** 2;
}
const sy = Math.sqrt(b / (I.length - 2));
const t = qt(p, I.length - 2);
diff --git a/src/marks/link.js b/src/marks/link.js
index b4d9529c7d..d3952db8a8 100644
--- a/src/marks/link.js
+++ b/src/marks/link.js
@@ -46,8 +46,8 @@ export class Link extends Mark {
const p = path();
const c = curve(p);
c.lineStart();
- c.point(X1[i], Y1[i]);
- c.point(X2[i], Y2[i]);
+ c.point(X1[i % X1.length], Y1[i % Y1.length]);
+ c.point(X2[i % X2.length], Y2[i % Y2.length]);
c.lineEnd();
return p;
})
diff --git a/src/marks/marker.js b/src/marks/marker.js
index c57a0f3642..ec5402d769 100644
--- a/src/marks/marker.js
+++ b/src/marks/marker.js
@@ -79,11 +79,11 @@ function markerCircleStroke(color, context) {
let nextMarkerId = 0;
export function applyMarkers(path, mark, {stroke: S} = {}) {
- return applyMarkersColor(path, mark, S && ((i) => S[i]));
+ return applyMarkersColor(path, mark, S && ((i) => S[i % S.length]));
}
export function applyGroupedMarkers(path, mark, {stroke: S} = {}) {
- return applyMarkersColor(path, mark, S && (([i]) => S[i]));
+ return applyMarkersColor(path, mark, S && (([i]) => S[i % S.length]));
}
function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke) {
diff --git a/src/marks/rect.js b/src/marks/rect.js
index b7a18b0192..8a33673132 100644
--- a/src/marks/rect.js
+++ b/src/marks/rect.js
@@ -66,18 +66,28 @@ export class Rect extends Mark {
.enter()
.append("rect")
.call(applyDirectStyles, this)
- .attr("x", X1 && X2 && !isCollapsed(x) ? (i) => Math.min(X1[i], X2[i]) + insetLeft : marginLeft + insetLeft)
- .attr("y", Y1 && Y2 && !isCollapsed(y) ? (i) => Math.min(Y1[i], Y2[i]) + insetTop : marginTop + insetTop)
+ .attr(
+ "x",
+ X1 && X2 && !isCollapsed(x)
+ ? (i) => Math.min(X1[i % X1.length], X2[i % X2.length]) + insetLeft
+ : marginLeft + insetLeft
+ )
+ .attr(
+ "y",
+ Y1 && Y2 && !isCollapsed(y)
+ ? (i) => Math.min(Y1[i % Y1.length], Y2[i % Y2.length]) + insetTop
+ : marginTop + insetTop
+ )
.attr(
"width",
X1 && X2 && !isCollapsed(x)
- ? (i) => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight)
+ ? (i) => Math.max(0, Math.abs(X2[i % X2.length] - X1[i % X1.length]) - insetLeft - insetRight)
: width - marginRight - marginLeft - insetRight - insetLeft
)
.attr(
"height",
Y1 && Y2 && !isCollapsed(y)
- ? (i) => Math.max(0, Math.abs(Y1[i] - Y2[i]) - insetTop - insetBottom)
+ ? (i) => Math.max(0, Math.abs(Y1[i % Y1.length] - Y2[i % Y2.length]) - insetTop - insetBottom)
: height - marginTop - marginBottom - insetTop - insetBottom
)
.call(applyAttr, "rx", rx)
diff --git a/src/marks/rule.js b/src/marks/rule.js
index caedd29d89..9f18e96dfb 100644
--- a/src/marks/rule.js
+++ b/src/marks/rule.js
@@ -42,15 +42,15 @@ export class RuleX extends Mark {
.enter()
.append("line")
.call(applyDirectStyles, this)
- .attr("x1", X ? (i) => X[i] : (marginLeft + width - marginRight) / 2)
- .attr("x2", X ? (i) => X[i] : (marginLeft + width - marginRight) / 2)
- .attr("y1", Y1 && !isCollapsed(y) ? (i) => Y1[i] + insetTop : marginTop + insetTop)
+ .attr("x1", X ? (i) => X[i % X.length] : (marginLeft + width - marginRight) / 2)
+ .attr("x2", X ? (i) => X[i % X.length] : (marginLeft + width - marginRight) / 2)
+ .attr("y1", Y1 && !isCollapsed(y) ? (i) => Y1[i % Y1.length] + insetTop : marginTop + insetTop)
.attr(
"y2",
Y2 && !isCollapsed(y)
? y.bandwidth
- ? (i) => Y2[i] + y.bandwidth() - insetBottom
- : (i) => Y2[i] - insetBottom
+ ? (i) => Y2[i % Y2.length] + y.bandwidth() - insetBottom
+ : (i) => Y2[i % Y2.length] - insetBottom
: height - marginBottom - insetBottom
)
.call(applyChannelStyles, this, channels)
@@ -90,17 +90,17 @@ export class RuleY extends Mark {
.enter()
.append("line")
.call(applyDirectStyles, this)
- .attr("x1", X1 && !isCollapsed(x) ? (i) => X1[i] + insetLeft : marginLeft + insetLeft)
+ .attr("x1", X1 && !isCollapsed(x) ? (i) => X1[i % X1.length] + insetLeft : marginLeft + insetLeft)
.attr(
"x2",
X2 && !isCollapsed(x)
? x.bandwidth
- ? (i) => X2[i] + x.bandwidth() - insetRight
- : (i) => X2[i] - insetRight
+ ? (i) => X2[i % X2.length] + x.bandwidth() - insetRight
+ : (i) => X2[i % X2.length] - insetRight
: width - marginRight - insetRight
)
- .attr("y1", Y ? (i) => Y[i] : (marginTop + height - marginBottom) / 2)
- .attr("y2", Y ? (i) => Y[i] : (marginTop + height - marginBottom) / 2)
+ .attr("y1", Y ? (i) => Y[i % Y.length] : (marginTop + height - marginBottom) / 2)
+ .attr("y2", Y ? (i) => Y[i % Y.length] : (marginTop + height - marginBottom) / 2)
.call(applyChannelStyles, this, channels)
)
.node();
diff --git a/src/marks/text.js b/src/marks/text.js
index 344e89c68f..c154487fea 100644
--- a/src/marks/text.js
+++ b/src/marks/text.js
@@ -101,29 +101,29 @@ export class Text extends Mark {
"transform",
R
? X && Y
- ? (i) => `translate(${X[i]},${Y[i]}) rotate(${R[i]})`
+ ? (i) => `translate(${X[i % X.length]},${Y[i % Y.length]}) rotate(${R[i % R.length]})`
: X
- ? (i) => `translate(${X[i]},${cy}) rotate(${R[i]})`
+ ? (i) => `translate(${X[i % X.length]},${cy}) rotate(${R[i % R.length]})`
: Y
- ? (i) => `translate(${cx},${Y[i]}) rotate(${R[i]})`
- : (i) => `translate(${cx},${cy}) rotate(${R[i]})`
+ ? (i) => `translate(${cx},${Y[i % Y.length] % Y.length}) rotate(${R[i % R.length]})`
+ : (i) => `translate(${cx},${cy}) rotate(${R[i % R.length]})`
: rotate
? X && Y
- ? (i) => `translate(${X[i]},${Y[i]}) rotate(${rotate})`
+ ? (i) => `translate(${X[i % X.length]},${Y[i % Y.length]}) rotate(${rotate})`
: X
- ? (i) => `translate(${X[i]},${cy}) rotate(${rotate})`
+ ? (i) => `translate(${X[i % X.length]},${cy}) rotate(${rotate})`
: Y
- ? (i) => `translate(${cx},${Y[i]}) rotate(${rotate})`
+ ? (i) => `translate(${cx},${Y[i % Y.length]}) rotate(${rotate})`
: `translate(${cx},${cy}) rotate(${rotate})`
: X && Y
- ? (i) => `translate(${X[i]},${Y[i]})`
+ ? (i) => `translate(${X[i % X.length]},${Y[i % Y.length]})`
: X
- ? (i) => `translate(${X[i]},${cy})`
+ ? (i) => `translate(${X[i % X.length]},${cy})`
: Y
- ? (i) => `translate(${cx},${Y[i]})`
+ ? (i) => `translate(${cx},${Y[i % Y.length]})`
: `translate(${cx},${cy})`
)
- .call(applyAttr, "font-size", FS && ((i) => FS[i]))
+ .call(applyAttr, "font-size", FS && ((i) => FS[i % FS.length]))
.call(applyChannelStyles, this, channels)
)
.node();
@@ -138,7 +138,7 @@ function applyMultilineText(selection, {monospace, lineAnchor, lineHeight, lineW
: (t) => lineWrap(t, lineWidth * 100, defaultWidth)
: (t) => t.split(/\r\n?|\n/g);
selection.each(function (i) {
- const lines = linesof(formatDefault(T[i]));
+ const lines = linesof(formatDefault(T[i % T.length]));
const n = lines.length;
const y = lineAnchor === "top" ? 0.71 : lineAnchor === "bottom" ? -0.29 - n : (164 - n * 100) / 200;
if (n > 1) {
diff --git a/src/marks/tick.js b/src/marks/tick.js
index 9303b7c46d..735d321f63 100644
--- a/src/marks/tick.js
+++ b/src/marks/tick.js
@@ -52,18 +52,18 @@ export class TickX extends AbstractTick {
selection.call(applyTransform, mark, {x}, offset, 0);
}
_x1(scales, {x: X}) {
- return (i) => X[i];
+ return (i) => X[i % X.length];
}
_x2(scales, {x: X}) {
- return (i) => X[i];
+ return (i) => X[i % X.length];
}
_y1({y}, {y: Y}, {marginTop}) {
const {insetTop} = this;
- return Y && y ? (i) => Y[i] + insetTop : marginTop + insetTop;
+ return Y && y ? (i) => Y[i % Y.length] + insetTop : marginTop + insetTop;
}
_y2({y}, {y: Y}, {height, marginBottom}) {
const {insetBottom} = this;
- return Y && y ? (i) => Y[i] + y.bandwidth() - insetBottom : height - marginBottom - insetBottom;
+ return Y && y ? (i) => Y[i % Y.length] + y.bandwidth() - insetBottom : height - marginBottom - insetBottom;
}
}
@@ -86,17 +86,17 @@ export class TickY extends AbstractTick {
}
_x1({x}, {x: X}, {marginLeft}) {
const {insetLeft} = this;
- return X && x ? (i) => X[i] + insetLeft : marginLeft + insetLeft;
+ return X && x ? (i) => X[i % X.length] + insetLeft : marginLeft + insetLeft;
}
_x2({x}, {x: X}, {width, marginRight}) {
const {insetRight} = this;
- return X && x ? (i) => X[i] + x.bandwidth() - insetRight : width - marginRight - insetRight;
+ return X && x ? (i) => X[i % X.length] + x.bandwidth() - insetRight : width - marginRight - insetRight;
}
_y1(scales, {y: Y}) {
- return (i) => Y[i];
+ return (i) => Y[i % Y.length];
}
_y2(scales, {y: Y}) {
- return (i) => Y[i];
+ return (i) => Y[i % Y.length];
}
}
diff --git a/src/marks/vector.js b/src/marks/vector.js
index 7dc00e5700..697c7c4701 100644
--- a/src/marks/vector.js
+++ b/src/marks/vector.js
@@ -43,10 +43,10 @@ export class Vector extends Mark {
const {x: X, y: Y, length: L, rotate: R} = channels;
const {length, rotate, anchor} = this;
const [cx, cy] = applyFrameAnchor(this, dimensions);
- const fl = L ? (i) => L[i] : () => length;
- const fr = R ? (i) => R[i] : () => rotate;
- const fx = X ? (i) => X[i] : () => cx;
- const fy = Y ? (i) => Y[i] : () => cy;
+ const fl = L ? (i) => L[i % L.length] : () => length;
+ const fr = R ? (i) => R[i % R.length] : () => rotate;
+ const fx = X ? (i) => X[i % X.length] : () => cx;
+ const fy = Y ? (i) => Y[i % Y.length] : () => cy;
const k = anchor === "start" ? 0 : anchor === "end" ? 1 : 0.5;
return create("svg:g", context)
.attr("fill", "none")
diff --git a/src/options.js b/src/options.js
index 94ca533c1a..ffadc73ffd 100644
--- a/src/options.js
+++ b/src/options.js
@@ -265,8 +265,8 @@ export function mid(x1, x2) {
const X1 = x1.transform(data);
const X2 = x2.transform(data);
return isTemporal(X1) || isTemporal(X2)
- ? map(X1, (_, i) => new Date((+X1[i] + +X2[i]) / 2))
- : map(X1, (_, i) => (+X1[i] + +X2[i]) / 2, Float64Array);
+ ? map(X1, (_, i) => new Date((+X1[i % X1.length] + +X2[i % X2.length]) / 2))
+ : map(X1, (_, i) => (+X1[i % X1.length] + +X2[i % X2.length]) / 2, Float64Array);
},
label: x1.label
};
diff --git a/src/plot.js b/src/plot.js
index 0e53efdce0..f6dbde7652 100644
--- a/src/plot.js
+++ b/src/plot.js
@@ -697,7 +697,7 @@ export class Mark {
const {filter = defined} = channels[name];
if (filter !== null) {
const value = values[name];
- index = index.filter((i) => filter(value[i]));
+ index = index.filter((i) => filter(value[i % value.length]));
}
}
return index;
@@ -813,14 +813,14 @@ function facetGroups(index, {fx, fy}) {
}
function facetGroup1(index, {value: F}) {
- return groups(index, (i) => F[i]);
+ return groups(index, (i) => F[i % F.length]);
}
function facetGroup2(index, {value: FX}, {value: FY}) {
return groups(
index,
- (i) => FX[i],
- (i) => FY[i]
+ (i) => FX[i % FX.length],
+ (i) => FY[i % FY.length]
).flatMap(([x, xgroup]) => xgroup.map(([y, ygroup]) => [[x, y], ygroup]));
}
diff --git a/src/style.js b/src/style.js
index 3547c50f1e..c70910372b 100644
--- a/src/style.js
+++ b/src/style.js
@@ -141,7 +141,7 @@ export function styles(
export function applyTitle(selection, L) {
if (L)
selection
- .filter((i) => nonempty(L[i]))
+ .filter((i) => nonempty(L[i % L.length]))
.append("title")
.call(applyText, L);
}
@@ -150,17 +150,17 @@ export function applyTitle(selection, L) {
export function applyTitleGroup(selection, L) {
if (L)
selection
- .filter(([i]) => nonempty(L[i]))
+ .filter(([i]) => nonempty(L[i % L.length]))
.append("title")
.call(applyTextGroup, L);
}
export function applyText(selection, T) {
- if (T) selection.text((i) => formatDefault(T[i]));
+ if (T) selection.text((i) => formatDefault(T[i % T.length]));
}
export function applyTextGroup(selection, T) {
- if (T) selection.text(([i]) => formatDefault(T[i]));
+ if (T) selection.text(([i]) => formatDefault(T[i % T.length]));
}
export function applyChannelStyles(
@@ -178,14 +178,14 @@ export function applyChannelStyles(
href: H
}
) {
- if (AL) applyAttr(selection, "aria-label", (i) => AL[i]);
- if (F) applyAttr(selection, "fill", (i) => F[i]);
- if (FO) applyAttr(selection, "fill-opacity", (i) => FO[i]);
- if (S) applyAttr(selection, "stroke", (i) => S[i]);
- if (SO) applyAttr(selection, "stroke-opacity", (i) => SO[i]);
- if (SW) applyAttr(selection, "stroke-width", (i) => SW[i]);
- if (O) applyAttr(selection, "opacity", (i) => O[i]);
- if (H) applyHref(selection, (i) => H[i], target);
+ if (AL) applyAttr(selection, "aria-label", (i) => AL[i % AL.length]);
+ if (F) applyAttr(selection, "fill", (i) => F[i % F.length]);
+ if (FO) applyAttr(selection, "fill-opacity", (i) => FO[i % FO.length]);
+ if (S) applyAttr(selection, "stroke", (i) => S[i % S.length]);
+ if (SO) applyAttr(selection, "stroke-opacity", (i) => SO[i % SO.length]);
+ if (SW) applyAttr(selection, "stroke-width", (i) => SW[i % SW.length]);
+ if (O) applyAttr(selection, "opacity", (i) => O[i % O.length]);
+ if (H) applyHref(selection, (i) => H[i % H.length], target);
applyTitle(selection, T);
}
@@ -204,14 +204,14 @@ export function applyGroupedChannelStyles(
href: H
}
) {
- if (AL) applyAttr(selection, "aria-label", ([i]) => AL[i]);
- if (F) applyAttr(selection, "fill", ([i]) => F[i]);
- if (FO) applyAttr(selection, "fill-opacity", ([i]) => FO[i]);
- if (S) applyAttr(selection, "stroke", ([i]) => S[i]);
- if (SO) applyAttr(selection, "stroke-opacity", ([i]) => SO[i]);
- if (SW) applyAttr(selection, "stroke-width", ([i]) => SW[i]);
- if (O) applyAttr(selection, "opacity", ([i]) => O[i]);
- if (H) applyHref(selection, ([i]) => H[i], target);
+ if (AL) applyAttr(selection, "aria-label", ([i]) => AL[i % AL.length]);
+ if (F) applyAttr(selection, "fill", ([i]) => F[i % F.length]);
+ if (FO) applyAttr(selection, "fill-opacity", ([i]) => FO[i % FO.length]);
+ if (S) applyAttr(selection, "stroke", ([i]) => S[i % S.length]);
+ if (SO) applyAttr(selection, "stroke-opacity", ([i]) => SO[i % SO.length]);
+ if (SW) applyAttr(selection, "stroke-width", ([i]) => SW[i % SW.length]);
+ if (O) applyAttr(selection, "opacity", ([i]) => O[i % O.length]);
+ if (H) applyHref(selection, ([i]) => H[i % H.length], target);
applyTitleGroup(selection, T);
}
@@ -230,7 +230,7 @@ function groupAesthetics({
}
export function groupZ(I, Z, z) {
- const G = group(I, (i) => Z[i]);
+ const G = group(I, (i) => Z[i % Z.length]);
if (z === undefined && G.size > I.length >> 1) {
warn(
`Warning: the implicit z channel has high cardinality. This may occur when the fill or stroke channel is associated with quantitative data rather than ordinal or categorical data. You can suppress this warning by setting the z option explicitly; if this data represents a single series, set z to null.`
@@ -251,7 +251,7 @@ export function* groupIndex(I, position, {z}, channels) {
out: for (const i of G) {
// If any channel has an undefined value for this index, skip it.
for (const c of C) {
- if (!defined(c[i])) {
+ if (!defined(c[i % c.length])) {
if (Gg) Gg.push(-1);
continue out;
}
@@ -261,7 +261,7 @@ export function* groupIndex(I, position, {z}, channels) {
// group. Yield the current group and start a new one.
if (Ag === undefined) {
if (Gg) yield Gg;
- (Ag = A.map((c) => keyof(c[i]))), (Gg = [i]);
+ (Ag = A.map((c) => keyof(c[i % c.length]))), (Gg = [i]);
continue;
}
@@ -270,10 +270,10 @@ export function* groupIndex(I, position, {z}, channels) {
// and start a new group of the current index.
Gg.push(i);
for (let j = 0; j < A.length; ++j) {
- const k = keyof(A[j][i]);
+ const k = keyof(A[j][i % A[j].length]);
if (k !== Ag[j]) {
yield Gg;
- (Ag = A.map((c) => keyof(c[i]))), (Gg = [i]);
+ (Ag = A.map((c) => keyof(c[i % c.length]))), (Gg = [i]);
continue out;
}
}
diff --git a/src/transforms/basic.js b/src/transforms/basic.js
index e0a9a29cc8..6d7799ecca 100644
--- a/src/transforms/basic.js
+++ b/src/transforms/basic.js
@@ -90,7 +90,7 @@ export function filter(test, options) {
function filterTransform(value) {
return (data, facets) => {
const V = valueof(data, value);
- return {data, facets: facets.map((I) => I.filter((i) => V[i]))};
+ return {data, facets: facets.map((I) => I.filter((i) => V[i % V.length]))};
};
}
@@ -177,7 +177,7 @@ function sortValue(value) {
if (!V) return {}; // ignore missing channel
V = V.value;
}
- const compareValue = (i, j) => order(V[i], V[j]);
+ const compareValue = (i, j) => order(V[i % V.length], V[j % V.length]);
return {data, facets: facets.map((I) => I.slice().sort(compareValue))};
};
}
diff --git a/src/transforms/bin.js b/src/transforms/bin.js
index 91ab1666d5..cad361d70e 100644
--- a/src/transforms/bin.js
+++ b/src/transforms/bin.js
@@ -1,7 +1,14 @@
-import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3";
+import {
+ bin as binner,
+ extent,
+ sum,
+ thresholdFreedmanDiaconis,
+ thresholdScott,
+ thresholdSturges,
+ utcTickInterval
+} from "d3";
import {
valueof,
- range,
identity,
maybeColumn,
maybeTuple,
@@ -161,6 +168,7 @@ function binn(
...("fill" in inputs && {fill: GF || fill}),
...("stroke" in inputs && {stroke: GS || stroke}),
...basic(options, (data, facets) => {
+ const cover = (bx || by) && merge(facets);
const K = valueof(data, k);
const Z = valueof(data, z);
const F = valueof(data, vfill);
@@ -172,8 +180,8 @@ function binn(
const GZ = Z && setGZ([]);
const GF = F && setGF([]);
const GS = S && setGS([]);
- const BX = bx ? bx(data) : [[, , (I) => I]];
- const BY = by ? by(data) : [[, , (I) => I]];
+ const BX = bx ? bx(data, cover) : [[, , (I) => I]];
+ const BY = by ? by(data, cover) : [[, , (I) => I]];
const BX1 = bx && setBX1([]);
const BX2 = bx && setBX2([]);
const BY1 = by && setBY1([]);
@@ -198,9 +206,9 @@ function binn(
groupFacet.push(i++);
groupData.push(reduceData.reduce(b, data, extent));
if (K) GK.push(k);
- if (Z) GZ.push(G === Z ? f : Z[b[0]]);
- if (F) GF.push(G === F ? f : F[b[0]]);
- if (S) GS.push(G === S ? f : S[b[0]]);
+ if (Z) GZ.push(G === Z ? f : Z[b[0] % Z.length]);
+ if (F) GF.push(G === F ? f : F[b[0] % F.length]);
+ if (S) GS.push(G === S ? f : S[b[0] % S.length]);
if (BX1) BX1.push(x1), BX2.push(x2);
if (BY1) BY1.push(y1), BY2.push(y2);
for (const o of outputs) o.reduce(b, extent);
@@ -248,9 +256,9 @@ function maybeBinValueTuple(options) {
function maybeBin(options) {
if (options == null) return;
const {value, cumulative, domain = extent, thresholds} = options;
- const bin = (data) => {
+ const bin = (data, cover) => {
let V = valueof(data, value, Array); // d3.bin prefers Array input
- const bin = binner().value((i) => V[i]);
+ const bin = binner().value((i) => V[i % V.length]);
if (isTemporal(V) || isTimeThresholds(thresholds)) {
V = V.map(coerceDate);
let [min, max] = typeof domain === "function" ? domain(V) : domain;
@@ -279,7 +287,7 @@ function maybeBin(options) {
}
bin.thresholds(t).domain(d);
}
- let bins = bin(range(data)).map(binset);
+ let bins = bin(cover).map(binset);
if (cumulative) bins = (cumulative < 0 ? bins.reverse() : bins).map(bincumset);
return bins.map(binfilter);
};
@@ -365,3 +373,11 @@ function binfilter([{x0, x1}, set]) {
function binempty() {
return new Uint32Array(0);
}
+
+function merge(facets) {
+ if (facets.length === 1) return facets[0];
+ const U = new Uint32Array(sum(facets, (f) => f.length));
+ let k = 0;
+ for (const f of facets) for (const i of f) U[k++] = i;
+ return U;
+}
diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js
index 513a7b1efa..9dc35c7315 100644
--- a/src/transforms/dodge.js
+++ b/src/transforms/dodge.js
@@ -3,6 +3,7 @@ import {finite, positive} from "../defined.js";
import {identity, maybeNamed, number, valueof} from "../options.js";
import {coerceNumbers} from "../scales.js";
import {initializer} from "./basic.js";
+import {facetExclusive} from "../facet.js";
const anchorXLeft = ({marginLeft}) => [1, marginLeft];
const anchorXRight = ({width, marginRight}) => [-1, width - marginRight];
@@ -88,23 +89,26 @@ function dodge(y, x, anchor, padding, options) {
if (sort === undefined && reverse === undefined) options.sort = {channel: "r", order: "descending"};
}
return initializer(options, function (data, facets, {[x]: X, r: R}, scales, dimensions) {
+ let n;
+ ({n, facets} = facetExclusive(facets, data.length));
+
if (!X) throw new Error(`missing channel: ${x}`);
X = coerceNumbers(valueof(X.value, scales[X.scale] || identity));
const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3;
if (R) R = coerceNumbers(valueof(R.value, scales[R.scale] || identity));
let [ky, ty] = anchor(dimensions);
const compare = ky ? compareAscending : compareSymmetric;
- const Y = new Float64Array(X.length);
- const radius = R ? (i) => R[i] : () => r;
+ const Y = new Float64Array(n);
+ const radius = R ? (i) => R[i % R.length] : () => r;
for (let I of facets) {
const tree = IntervalTree();
- I = I.filter(R ? (i) => finite(X[i]) && positive(R[i]) : (i) => finite(X[i]));
+ I = I.filter(R ? (i) => finite(X[i % X.length]) && positive(R[i % R.length]) : (i) => finite(X[i % X.length]));
const intervals = new Float64Array(2 * I.length + 2);
for (const i of I) {
const ri = radius(i);
const y0 = ky ? ri + padding : 0; // offset baseline for varying radius
- const l = X[i] - ri;
- const h = X[i] + ri;
+ const l = X[i % X.length] - ri;
+ const h = X[i % X.length] + ri;
// The first two positions are 0 to test placing the dot on the baseline.
let k = 2;
@@ -113,9 +117,9 @@ function dodge(y, x, anchor, padding, options) {
// the y-positions that place this circle tangent to these other circles.
// https://observablehq.com/@mbostock/circle-offset-along-line
tree.queryInterval(l - padding, h + padding, ([, , j]) => {
- const yj = Y[j] - y0;
- const dx = X[i] - X[j];
- const dr = padding + (R ? R[i] + R[j] : 2 * r);
+ const yj = Y[j % Y.length] - y0;
+ const dx = X[i % X.length] - X[j % X.length];
+ const dr = padding + (R ? R[i % R.length] + R[j % R.length] : 2 * r);
const dy = Math.sqrt(dr * dr - dx * dx);
intervals[k++] = yj - dy;
intervals[k++] = yj + dy;
@@ -130,7 +134,7 @@ function dodge(y, x, anchor, padding, options) {
continue out;
}
}
- Y[i] = y + y0;
+ Y[i % Y.length] = y + y0;
break;
}
@@ -141,7 +145,7 @@ function dodge(y, x, anchor, padding, options) {
if (!ky) ky = 1;
for (const I of facets) {
for (const i of I) {
- Y[i] = Y[i] * ky + ty;
+ Y[i % Y.length] = Y[i % Y.length] * ky + ty;
}
}
return {
diff --git a/src/transforms/group.js b/src/transforms/group.js
index 5aa82be38a..11738c0b6c 100644
--- a/src/transforms/group.js
+++ b/src/transforms/group.js
@@ -175,9 +175,9 @@ function groupn(
groupData.push(reduceData.reduce(g, data));
if (X) GX.push(x);
if (Y) GY.push(y);
- if (Z) GZ.push(G === Z ? f : Z[g[0]]);
- if (F) GF.push(G === F ? f : F[g[0]]);
- if (S) GS.push(G === S ? f : S[g[0]]);
+ if (Z) GZ.push(G === Z ? f : Z[g[0] % Z.length]);
+ if (F) GF.push(G === F ? f : F[g[0] % F.length]);
+ if (S) GS.push(G === S ? f : S[g[0] % S.length]);
for (const o of outputs) o.reduce(g);
if (sort) sort.reduce(g);
}
@@ -259,7 +259,7 @@ export function maybeEvaluator(name, reduce, inputs) {
export function maybeGroup(I, X) {
return X
? sort(
- grouper(I, (i) => X[i]),
+ grouper(I, (i) => X[i % X.length]),
first
)
: [[, I]];
@@ -330,7 +330,7 @@ export function maybeSubgroup(outputs, inputs) {
export function maybeSort(facets, sort, reverse) {
if (sort) {
const S = sort.output.transform();
- const compare = (i, j) => ascendingDefined(S[i], S[j]);
+ const compare = (i, j) => ascendingDefined(S[i % S.length], S[j % S.length]);
facets.forEach((f) => f.sort(compare));
}
if (reverse) {
@@ -349,7 +349,7 @@ function reduceFunction(f) {
function reduceAccessor(f) {
return {
reduce(I, X) {
- return f(I, (i) => X[i]);
+ return f(I, (i) => X[i % X.length]);
}
};
}
@@ -373,7 +373,7 @@ const reduceTitle = {
rollup(
I,
(V) => V.length,
- (i) => X[i]
+ (i) => X[i % X.length]
),
second
);
@@ -403,7 +403,7 @@ const reduceDistinct = {
label: "Distinct",
reduce: (I, X) => {
const s = new InternSet();
- for (const i of I) s.add(X[i]);
+ for (const i of I) s.add(X[i % X.length]);
return s.size;
}
};
@@ -413,7 +413,7 @@ const reduceSum = reduceAccessor(sum);
function reduceProportion(value, scope) {
return value == null
? {scope, label: "Frequency", reduce: (I, V, basis = 1) => I.length / basis}
- : {scope, reduce: (I, V, basis = 1) => sum(I, (i) => V[i]) / basis};
+ : {scope, reduce: (I, V, basis = 1) => sum(I, (i) => V[i % V.length]) / basis};
}
function mid(x1, x2) {
diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js
index ebd0e50dc8..26d92d15c9 100644
--- a/src/transforms/hexbin.js
+++ b/src/transforms/hexbin.js
@@ -109,10 +109,10 @@ export function hexbin(outputs = {fill: "count"}, options = {}) {
binFacet.push(++i);
BX.push(bin.x);
BY.push(bin.y);
- if (Z) GZ.push(G === Z ? f : Z[bin[0]]);
- if (F) GF.push(G === F ? f : F[bin[0]]);
- if (S) GS.push(G === S ? f : S[bin[0]]);
- if (Q) GQ.push(G === Q ? f : Q[bin[0]]);
+ if (Z) GZ.push(G === Z ? f : Z[bin[0] % Z.length]);
+ if (F) GF.push(G === F ? f : F[bin[0] % F.length]);
+ if (S) GS.push(G === S ? f : S[bin[0] % S.length]);
+ if (Q) GQ.push(G === Q ? f : Q[bin[0] % Q.length]);
for (const o of outputs) o.reduce(bin);
}
}
@@ -143,8 +143,8 @@ function hbin(I, X, Y, dx) {
const dy = dx * (1.5 / sqrt3);
const bins = new Map();
for (const i of I) {
- let px = X[i],
- py = Y[i];
+ let px = X[i % X.length],
+ py = Y[i % Y.length];
if (isNaN(px) || isNaN(py)) continue;
let pj = Math.round((py = (py - oy) / dy)),
pi = Math.round((px = (px - ox) / dx - (pj & 1) / 2)),
diff --git a/src/transforms/map.js b/src/transforms/map.js
index b62ea564c0..e3b34cf7c8 100644
--- a/src/transforms/map.js
+++ b/src/transforms/map.js
@@ -1,6 +1,7 @@
import {count, group, rank} from "d3";
import {maybeZ, take, valueof, maybeInput, column} from "../options.js";
import {basic} from "./basic.js";
+import {facetExclusive} from "../facet.js";
/**
* ```js
@@ -58,12 +59,14 @@ export function map(outputs = {}, options = {}) {
});
return {
...basic(options, (data, facets) => {
+ let n;
+ ({n, facets} = facetExclusive(facets, data.length));
const Z = valueof(data, z);
- const X = channels.map(({input}) => valueof(data, input));
- const MX = channels.map(({setOutput}) => setOutput(new Array(data.length)));
+ const Xs = channels.map(({input}) => valueof(data, input));
+ const MX = channels.map(({setOutput}) => setOutput(new Array(n)));
for (const facet of facets) {
- for (const I of Z ? group(facet, (i) => Z[i]).values() : [facet]) {
- channels.forEach(({map}, i) => map.map(I, X[i], MX[i]));
+ for (const I of Z ? group(facet, (i) => Z[i % Z.length]).values() : [facet]) {
+ channels.forEach(({map}, k) => map.map(I, Xs[k], MX[k]));
}
}
return {data, facets};
@@ -97,9 +100,14 @@ function rankQuantile(V) {
function mapFunction(f) {
return {
map(I, S, T) {
- const M = f(take(S, I));
+ const M = f(
+ take(
+ S,
+ I.map((i) => i % S.length)
+ )
+ );
if (M.length !== I.length) throw new Error("map function returned a mismatched length");
- for (let i = 0, n = I.length; i < n; ++i) T[I[i]] = M[i];
+ for (let i = 0, n = I.length; i < n; ++i) T[I[i % I.length]] = M[i % M.length];
}
};
}
@@ -107,6 +115,6 @@ function mapFunction(f) {
const mapCumsum = {
map(I, S, T) {
let sum = 0;
- for (const i of I) T[i] = sum += S[i];
+ for (const i of I) T[i] = sum += S[i % S.length];
}
};
diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js
index a997095ab9..d275a6f43f 100644
--- a/src/transforms/normalize.js
+++ b/src/transforms/normalize.js
@@ -71,46 +71,46 @@ function normalizeBasis(basis) {
map(I, S, T) {
const b = +basis(I, S);
for (const i of I) {
- T[i] = S[i] === null ? NaN : S[i] / b;
+ T[i % T.length] = S[i % S.length] === null ? NaN : S[i % S.length] / b;
}
}
};
}
function normalizeAccessor(f) {
- return normalizeBasis((I, S) => f(I, (i) => S[i]));
+ return normalizeBasis((I, S) => f(I, (i) => S[i % S.length]));
}
const normalizeExtent = {
map(I, S, T) {
- const [s1, s2] = extent(I, (i) => S[i]),
+ const [s1, s2] = extent(I, (i) => S[i % S.length]),
d = s2 - s1;
for (const i of I) {
- T[i] = S[i] === null ? NaN : (S[i] - s1) / d;
+ T[i % T.length] = S[i % S.length] === null ? NaN : (S[i % S.length] - s1) / d;
}
}
};
const normalizeFirst = normalizeBasis((I, S) => {
for (let i = 0; i < I.length; ++i) {
- const s = S[I[i]];
+ const s = S[I[i % I.length]];
if (defined(s)) return s;
}
});
const normalizeLast = normalizeBasis((I, S) => {
for (let i = I.length - 1; i >= 0; --i) {
- const s = S[I[i]];
+ const s = S[I[i % I.length]];
if (defined(s)) return s;
}
});
const normalizeDeviation = {
map(I, S, T) {
- const m = mean(I, (i) => S[i]);
- const d = deviation(I, (i) => S[i]);
+ const m = mean(I, (i) => S[i % S.length]);
+ const d = deviation(I, (i) => S[i % S.length]);
for (const i of I) {
- T[i] = S[i] === null ? NaN : d ? (S[i] - m) / d : 0;
+ T[i % T.length] = S[i % S.length] === null ? NaN : d ? (S[i % S.length] - m) / d : 0;
}
}
};
diff --git a/src/transforms/select.js b/src/transforms/select.js
index 5f972851d3..2fe7fa41f3 100644
--- a/src/transforms/select.js
+++ b/src/transforms/select.js
@@ -129,11 +129,11 @@ function* selectorLast(I) {
}
function* selectorMin(I, X) {
- yield least(I, (i) => X[i]);
+ yield least(I, (i) => X[i % X.length]);
}
function* selectorMax(I, X) {
- yield greatest(I, (i) => X[i]);
+ yield greatest(I, (i) => X[i % X.length]);
}
function selectChannel(v, selector, options) {
@@ -148,7 +148,7 @@ function selectChannel(v, selector, options) {
const selectFacets = [];
for (const facet of facets) {
const selectFacet = [];
- for (const I of Z ? group(facet, (i) => Z[i]).values() : [facet]) {
+ for (const I of Z ? group(facet, (i) => Z[i % Z.length]).values() : [facet]) {
for (const i of selector(I, V)) {
selectFacet.push(i);
}
diff --git a/src/transforms/stack.js b/src/transforms/stack.js
index 7018ff5ce9..672808bad4 100644
--- a/src/transforms/stack.js
+++ b/src/transforms/stack.js
@@ -1,6 +1,7 @@
import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3";
import {ascendingDefined} from "../defined.js";
import {field, column, maybeColumn, maybeZ, mid, range, valueof, maybeZero, one} from "../options.js";
+import {facetExclusive} from "../facet.js";
import {basic} from "./basic.js";
/**
@@ -147,23 +148,24 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) {
order = maybeOrder(order, offset, ky);
return [
basic(options, (data, facets) => {
+ let n;
+ ({n, facets} = facetExclusive(facets, data.length));
const X = x == null ? undefined : setX(valueof(data, x));
const Y = valueof(data, y, Float64Array);
const Z = valueof(data, z);
const O = order && order(data, X, Y, Z);
- const n = data.length;
const Y1 = setY1(new Float64Array(n));
const Y2 = setY2(new Float64Array(n));
const facetstacks = [];
for (const facet of facets) {
- const stacks = X ? Array.from(group(facet, (i) => X[i]).values()) : [facet];
+ const stacks = X ? Array.from(group(facet, (i) => X[i % X.length]).values()) : [facet];
if (O) applyOrder(stacks, O);
for (const stack of stacks) {
let yn = 0,
yp = 0;
if (reverse) stack.reverse();
for (const i of stack) {
- const y = Y[i];
+ const y = Y[i % Y.length];
if (y < 0) yn = Y2[i] = (Y1[i] = yn) + y;
else if (y > 0) yp = Y2[i] = (Y1[i] = yp) + y;
else Y2[i] = Y1[i] = yp; // NaN or zero
@@ -206,7 +208,7 @@ function extent(stack, Y2) {
let min = 0,
max = 0;
for (const i of stack) {
- const y = Y2[i];
+ const y = Y2[i % Y2.length];
if (y < min) min = y;
if (y > max) max = y;
}
@@ -219,8 +221,8 @@ function offsetExpand(facetstacks, Y1, Y2) {
const [yn, yp] = extent(stack, Y2);
for (const i of stack) {
const m = 1 / (yp - yn || 1);
- Y1[i] = m * (Y1[i] - yn);
- Y2[i] = m * (Y2[i] - yn);
+ Y1[i % Y1.length] = m * (Y1[i % Y1.length] - yn);
+ Y2[i % Y2.length] = m * (Y2[i % Y2.length] - yn);
}
}
}
@@ -232,8 +234,8 @@ function offsetCenter(facetstacks, Y1, Y2) {
const [yn, yp] = extent(stack, Y2);
for (const i of stack) {
const m = (yp + yn) / 2;
- Y1[i] -= m;
- Y2[i] -= m;
+ Y1[i % Y1.length] -= m;
+ Y2[i % Y2.length] -= m;
}
}
offsetZero(stacks, Y1, Y2);
@@ -247,18 +249,18 @@ function offsetWiggle(facetstacks, Y1, Y2, Z) {
let y = 0;
for (const stack of stacks) {
let j = -1;
- const Fi = stack.map((i) => Math.abs(Y2[i] - Y1[i]));
+ const Fi = stack.map((i) => Math.abs(Y2[i % Y2.length] - Y1[i % Y1.length]));
const Df = stack.map((i) => {
- j = Z ? Z[i] : ++j;
- const value = Y2[i] - Y1[i];
+ j = Z ? Z[i % Z.length] : ++j;
+ const value = Y2[i % Y2.length] - Y1[i % Y1.length];
const diff = prev.has(j) ? value - prev.get(j) : 0;
prev.set(j, value);
return diff;
});
const Cf1 = [0, ...cumsum(Df)];
for (const i of stack) {
- Y1[i] += y;
- Y2[i] += y;
+ Y1[i % Y1.length] += y;
+ Y2[i % Y2.length] += y;
}
const s1 = sum(Fi);
if (s1) y -= sum(Fi, (d, i) => (Df[i] / 2 + Cf1[i]) * d) / s1;
@@ -269,11 +271,11 @@ function offsetWiggle(facetstacks, Y1, Y2, Z) {
}
function offsetZero(stacks, Y1, Y2) {
- const m = min(stacks, (stack) => min(stack, (i) => Y1[i]));
+ const m = min(stacks, (stack) => min(stack, (i) => Y1[i % Y1.length]));
for (const stack of stacks) {
for (const i of stack) {
- Y1[i] -= m;
- Y2[i] -= m;
+ Y1[i % Y1.length] -= m;
+ Y2[i % Y2.length] -= m;
}
}
}
@@ -282,13 +284,13 @@ function offsetCenterFacets(facetstacks, Y1, Y2) {
const n = facetstacks.length;
if (n === 1) return;
const facets = facetstacks.map((stacks) => stacks.flat());
- const m = facets.map((I) => (min(I, (i) => Y1[i]) + max(I, (i) => Y2[i])) / 2);
+ const m = facets.map((I) => (min(I, (i) => Y1[i % Y1.length]) + max(I, (i) => Y2[i % Y2.length])) / 2);
const m0 = min(m);
for (let j = 0; j < n; j++) {
const p = m0 - m[j];
for (const i of facets[j]) {
- Y1[i] += p;
- Y2[i] += p;
+ Y1[i % Y1.length] += p;
+ Y2[i % Y2.length] += p;
}
}
}
@@ -332,9 +334,9 @@ function orderSum(data, X, Y, Z) {
return orderZDomain(
Z,
groupSort(
- range(data),
+ range(Y),
(I) => sum(I, (i) => Y[i]),
- (i) => Z[i]
+ (i) => Z[i % Z.length]
)
);
}
@@ -344,9 +346,9 @@ function orderAppearance(data, X, Y, Z) {
return orderZDomain(
Z,
groupSort(
- range(data),
- (I) => X[greatest(I, (i) => Y[i])],
- (i) => Z[i]
+ range(Y),
+ (I) => X[greatest(I, (i) => Y[i]) % X.length],
+ (i) => Z[i % Z.length]
)
);
}
@@ -354,16 +356,16 @@ function orderAppearance(data, X, Y, Z) {
// by x = argmax of value, but rearranged inside-out by alternating series
// according to the sign of a running divergence of sums
function orderInsideOut(data, X, Y, Z) {
- const I = range(data);
+ const I = range(Y);
const K = groupSort(
I,
(I) => X[greatest(I, (i) => Y[i])],
- (i) => Z[i]
+ (i) => Z[i % Z.length]
);
const sums = rollup(
I,
(I) => sum(I, (i) => Y[i]),
- (i) => Z[i]
+ (i) => Z[i % Z.length]
);
const Kp = [],
Kn = [];
@@ -398,6 +400,6 @@ function orderZDomain(Z, domain) {
function applyOrder(stacks, O) {
for (const stack of stacks) {
- stack.sort((i, j) => ascendingDefined(O[i], O[j]));
+ stack.sort((i, j) => ascendingDefined(O[i % O.length], O[j % O.length]));
}
}
diff --git a/src/transforms/tree.js b/src/transforms/tree.js
index 830d6a5c6c..45cf4a3181 100644
--- a/src/transforms/tree.js
+++ b/src/transforms/tree.js
@@ -56,14 +56,14 @@ export function treeNode(options = {}) {
let treeIndex = -1;
const treeData = [];
const treeFacets = [];
- const rootof = stratify().path((i) => P[i]);
+ const rootof = stratify().path((i) => P[i % P.length]);
const layout = treeLayout();
if (layout.nodeSize) layout.nodeSize([1, 1]);
if (layout.separation && treeSeparation !== undefined) layout.separation(treeSeparation ?? one);
for (const o of outputs) o[output_values] = o[output_setValues]([]);
for (const facet of facets) {
const treeFacet = [];
- const root = rootof(facet.filter((i) => P[i] != null)).each((node) => (node.data = data[node.data]));
+ const root = rootof(facet.filter((i) => P[i % P.length] != null)).each((node) => (node.data = data[node.data]));
if (treeSort != null) root.sort(treeSort);
layout(root);
for (const node of root.descendants()) {
@@ -150,14 +150,14 @@ export function treeLink(options = {}) {
let treeIndex = -1;
const treeData = [];
const treeFacets = [];
- const rootof = stratify().path((i) => P[i]);
+ const rootof = stratify().path((i) => P[i % P.length]);
const layout = treeLayout();
if (layout.nodeSize) layout.nodeSize([1, 1]);
if (layout.separation && treeSeparation !== undefined) layout.separation(treeSeparation ?? one);
for (const o of outputs) o[output_values] = o[output_setValues]([]);
for (const facet of facets) {
const treeFacet = [];
- const root = rootof(facet.filter((i) => P[i] != null)).each((node) => (node.data = data[node.data]));
+ const root = rootof(facet.filter((i) => P[i % P.length] != null)).each((node) => (node.data = data[node.data]));
if (treeSort != null) root.sort(treeSort);
layout(root);
for (const {source, target} of root.links()) {
diff --git a/src/transforms/window.js b/src/transforms/window.js
index a0300c8085..9db9b41c9e 100644
--- a/src/transforms/window.js
+++ b/src/transforms/window.js
@@ -96,19 +96,19 @@ function reduceNumbers(f) {
strict
? {
map(I, S, T) {
- const C = Float64Array.from(I, (i) => (S[i] === null ? NaN : S[i]));
+ const C = Float64Array.from(I, (i) => (S[i % S.length] === null ? NaN : S[i % S.length]));
let nans = 0;
- for (let i = 0; i < k - 1; ++i) if (isNaN(C[i])) ++nans;
+ for (let i = 0; i < k - 1; ++i) if (isNaN(C[i % C.length])) ++nans;
for (let i = 0, n = I.length - k + 1; i < n; ++i) {
if (isNaN(C[i + k - 1])) ++nans;
T[I[i + s]] = nans === 0 ? f(C.subarray(i, i + k)) : NaN;
- if (isNaN(C[i])) --nans;
+ if (isNaN(C[i % C.length])) --nans;
}
}
}
: {
map(I, S, T) {
- const C = Float64Array.from(I, (i) => (S[i] === null ? NaN : S[i]));
+ const C = Float64Array.from(I, (i) => (S[i % S.length] === null ? NaN : S[i % S.length]));
for (let i = -s; i < 0; ++i) {
T[I[i + s]] = f(C.subarray(0, i + k));
}
@@ -125,11 +125,11 @@ function reduceArray(f) {
? {
map(I, S, T) {
let count = 0;
- for (let i = 0; i < k - 1; ++i) count += defined(S[I[i]]);
+ for (let i = 0; i < k - 1; ++i) count += defined(S[I[i % I.length]]);
for (let i = 0, n = I.length - k + 1; i < n; ++i) {
count += defined(S[I[i + k - 1]]);
if (count === k) T[I[i + s]] = f(take(S, slice(I, i, i + k)));
- count -= defined(S[I[i]]);
+ count -= defined(S[I[i % I.length]]);
}
}
}
@@ -152,12 +152,12 @@ function reduceSum(k, s, strict) {
let nans = 0;
let sum = 0;
for (let i = 0; i < k - 1; ++i) {
- const v = S[I[i]];
+ const v = S[I[i % I.length]];
if (v === null || isNaN(v)) ++nans;
else sum += +v;
}
for (let i = 0, n = I.length - k + 1; i < n; ++i) {
- const a = S[I[i]];
+ const a = S[I[i % I.length]];
const b = S[I[i + k - 1]];
if (b === null || isNaN(b)) ++nans;
else sum += +b;
@@ -172,12 +172,12 @@ function reduceSum(k, s, strict) {
let sum = 0;
const n = I.length;
for (let i = 0, j = Math.min(n, k - s - 1); i < j; ++i) {
- sum += +S[I[i]] || 0;
+ sum += +S[I[i % I.length]] || 0;
}
for (let i = -s, j = n - s; i < j; ++i) {
sum += +S[I[i + k - 1]] || 0;
T[I[i + s]] = sum;
- sum -= +S[I[i]] || 0;
+ sum -= +S[I[i % I.length]] || 0;
}
}
};
@@ -201,12 +201,12 @@ function reduceMean(k, s, strict) {
let count = 0;
const n = I.length;
for (let i = 0, j = Math.min(n, k - s - 1); i < j; ++i) {
- let v = S[I[i]];
+ let v = S[I[i] % S.length];
if (v !== null && !isNaN((v = +v))) (sum += v), ++count;
}
for (let i = -s, j = n - s; i < j; ++i) {
- let a = S[I[i + k - 1]];
- let b = S[I[i]];
+ let a = S[I[i + k - 1] % S.length];
+ let b = S[I[i] % S.length];
if (a !== null && !isNaN((a = +a))) (sum += a), ++count;
T[I[i + s]] = sum / count;
if (b !== null && !isNaN((b = +b))) (sum -= b), --count;
@@ -218,28 +218,28 @@ function reduceMean(k, s, strict) {
function firstDefined(S, I, i, k) {
for (let j = i + k; i < j; ++i) {
- const v = S[I[i]];
+ const v = S[I[i] % S.length];
if (defined(v)) return v;
}
}
function lastDefined(S, I, i, k) {
for (let j = i + k - 1; j >= i; --j) {
- const v = S[I[j]];
+ const v = S[I[j] % S.length];
if (defined(v)) return v;
}
}
function firstNumber(S, I, i, k) {
for (let j = i + k; i < j; ++i) {
- let v = S[I[i]];
+ let v = S[I[i] % S.length];
if (v !== null && !isNaN((v = +v))) return v;
}
}
function lastNumber(S, I, i, k) {
for (let j = i + k - 1; j >= i; --j) {
- let v = S[I[j]];
+ let v = S[I[j] % S.length];
if (v !== null && !isNaN((v = +v))) return v;
}
}
@@ -249,8 +249,8 @@ function reduceDifference(k, s, strict) {
? {
map(I, S, T) {
for (let i = 0, n = I.length - k; i < n; ++i) {
- const a = S[I[i]];
- const b = S[I[i + k - 1]];
+ const a = S[I[i] % S.length];
+ const b = S[I[i + k - 1] % S.length];
T[I[i + s]] = a === null || b === null ? NaN : b - a;
}
}
@@ -269,8 +269,8 @@ function reduceRatio(k, s, strict) {
? {
map(I, S, T) {
for (let i = 0, n = I.length - k; i < n; ++i) {
- const a = S[I[i]];
- const b = S[I[i + k - 1]];
+ const a = S[I[i] % I.length];
+ const b = S[I[i + k - 1] % I.length];
T[I[i + s]] = a === null || b === null ? NaN : b / a;
}
}
@@ -289,7 +289,7 @@ function reduceFirst(k, s, strict) {
? {
map(I, S, T) {
for (let i = 0, n = I.length - k; i < n; ++i) {
- T[I[i + s]] = S[I[i]];
+ T[I[i + s]] = S[I[i] % I.length];
}
}
}
@@ -307,7 +307,7 @@ function reduceLast(k, s, strict) {
? {
map(I, S, T) {
for (let i = 0, n = I.length - k; i < n; ++i) {
- T[I[i + s]] = S[I[i + k - 1]];
+ T[I[i + s]] = S[I[i + k - 1] % S.length];
}
}
}
diff --git a/test/output/generativeRoses.svg b/test/output/generativeRoses.svg
new file mode 100644
index 0000000000..e4a7205c79
--- /dev/null
+++ b/test/output/generativeRoses.svg
@@ -0,0 +1,296 @@
+
\ No newline at end of file
diff --git a/test/output/hexbinExclude.svg b/test/output/hexbinExclude.svg
new file mode 100644
index 0000000000..ca4edb5e0f
--- /dev/null
+++ b/test/output/hexbinExclude.svg
@@ -0,0 +1,401 @@
+
\ No newline at end of file
diff --git a/test/output/musicRevenueBars.svg b/test/output/musicRevenueBars.svg
new file mode 100644
index 0000000000..67d0421b08
--- /dev/null
+++ b/test/output/musicRevenueBars.svg
@@ -0,0 +1,9986 @@
+
\ No newline at end of file
diff --git a/test/output/musicRevenueBin.svg b/test/output/musicRevenueBin.svg
new file mode 100644
index 0000000000..a55a967481
--- /dev/null
+++ b/test/output/musicRevenueBin.svg
@@ -0,0 +1,2318 @@
+
\ No newline at end of file
diff --git a/test/output/musicRevenueGroup.svg b/test/output/musicRevenueGroup.svg
new file mode 100644
index 0000000000..de3afad1ad
--- /dev/null
+++ b/test/output/musicRevenueGroup.svg
@@ -0,0 +1,484 @@
+
\ No newline at end of file
diff --git a/test/output/musicRevenueWiggle.svg b/test/output/musicRevenueWiggle.svg
new file mode 100644
index 0000000000..f446964356
--- /dev/null
+++ b/test/output/musicRevenueWiggle.svg
@@ -0,0 +1,412 @@
+
\ No newline at end of file
diff --git a/test/output/penguinCumsumExclude.svg b/test/output/penguinCumsumExclude.svg
new file mode 100644
index 0000000000..3e0fb57005
--- /dev/null
+++ b/test/output/penguinCumsumExclude.svg
@@ -0,0 +1,806 @@
+
\ No newline at end of file
diff --git a/test/output/penguinDodgeReindexed.svg b/test/output/penguinDodgeReindexed.svg
new file mode 100644
index 0000000000..c6a90c6451
--- /dev/null
+++ b/test/output/penguinDodgeReindexed.svg
@@ -0,0 +1,746 @@
+
\ No newline at end of file
diff --git a/test/output/stackExclude.svg b/test/output/stackExclude.svg
new file mode 100644
index 0000000000..1631981e99
--- /dev/null
+++ b/test/output/stackExclude.svg
@@ -0,0 +1,68 @@
+
\ No newline at end of file
diff --git a/test/plots/generative-roses.js b/test/plots/generative-roses.js
new file mode 100644
index 0000000000..cb9d2df848
--- /dev/null
+++ b/test/plots/generative-roses.js
@@ -0,0 +1,40 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+// Generate roses from a cumulative sum of vectors with various angles. The
+// twist is that each facet selects a subset of these angles to ignore (with
+// facet: "exclude").
+export default async function () {
+ const data = d3.range(0, 48, 0.7);
+ const mapped = Plot.mapY(
+ "cumsum",
+ Plot.mapX("cumsum", {
+ facet: "exclude",
+ x: Math.sin,
+ y: Math.cos
+ })
+ );
+ return Plot.plot({
+ facet: {
+ data,
+ x: (d, i) => (i % 8) % 3,
+ y: (d, i) => Math.floor((i % 8) / 3),
+ marginRight: 80
+ },
+ axis: null,
+ marks: [
+ Plot.line(data, {...mapped, curve: "natural"}),
+ ["First", "Last", "MaxX", "MinX", "MaxY", "MinY"].map((p) =>
+ Plot.dot(
+ data,
+ Plot[`select${p}`]({
+ ...mapped,
+ fill: () => p,
+ title: () => p,
+ r: 6
+ })
+ )
+ )
+ ]
+ });
+}
diff --git a/test/plots/hexbin-exclude.js b/test/plots/hexbin-exclude.js
new file mode 100644
index 0000000000..c862a43cf0
--- /dev/null
+++ b/test/plots/hexbin-exclude.js
@@ -0,0 +1,45 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const penguins = await d3.csv("data/penguins.csv", d3.autoType);
+ const noise = d3.randomNormal.source(d3.randomLcg(42))(0, 0.1);
+ return Plot.plot({
+ width: 960,
+ height: 320,
+ inset: 14,
+ facet: {
+ data: penguins,
+ x: "species",
+ marginRight: 80
+ },
+ marks: [
+ Plot.frame(),
+ Plot.dot(
+ penguins,
+ Plot.hexbin(
+ {fillOpacity: "count"},
+ Plot.map(
+ {
+ x: (X) => X.map((d) => d + noise()),
+ y: (Y) => Y.map((d) => d + noise())
+ },
+ {
+ x: "culmen_depth_mm",
+ y: "culmen_length_mm",
+ fill: "species",
+ facet: "exclude"
+ }
+ )
+ )
+ ),
+ Plot.dot(
+ penguins,
+ Plot.hexbin(
+ {fillOpacity: "count"},
+ {x: "culmen_depth_mm", y: "culmen_length_mm", fill: "species", stroke: "species"}
+ )
+ )
+ ]
+ });
+}
diff --git a/test/plots/index.js b/test/plots/index.js
index 9a94cecfee..dbd041ec4c 100644
--- a/test/plots/index.js
+++ b/test/plots/index.js
@@ -75,6 +75,7 @@ export {default as footballCoverage} from "./football-coverage.js";
export {default as frameCorners} from "./frame-corners.js";
export {default as fruitSales} from "./fruit-sales.js";
export {default as fruitSalesDate} from "./fruit-sales-date.js";
+export {default as generativeRoses} from "./generative-roses.js";
export {default as gistempAnomaly} from "./gistemp-anomaly.js";
export {default as gistempAnomalyMoving} from "./gistemp-anomaly-moving.js";
export {default as gistempAnomalyTransform} from "./gistemp-anomaly-transform.js";
@@ -85,6 +86,7 @@ export {default as gridChoroplethDx} from "./grid-choropleth-dx.js";
export {default as groupedRects} from "./grouped-rects.js";
export {default as hadcrutWarmingStripes} from "./hadcrut-warming-stripes.js";
export {default as hexbin} from "./hexbin.js";
+export {default as hexbinExclude} from "./hexbin-exclude.js";
export {default as hexbinOranges} from "./hexbin-oranges.js";
export {default as hexbinR} from "./hexbin-r.js";
export {default as hexbinSymbol} from "./hexbin-symbol.js";
@@ -134,6 +136,10 @@ export {default as mobyDickLetterRelativeFrequency} from "./moby-dick-letter-rel
export {default as morleyBoxplot} from "./morley-boxplot.js";
export {default as moviesProfitByGenre} from "./movies-profit-by-genre.js";
export {default as musicRevenue} from "./music-revenue.js";
+export {default as musicRevenueBars} from "./music-revenue-bars.js";
+export {default as musicRevenueBin} from "./music-revenue-bin.js";
+export {default as musicRevenueGroup} from "./music-revenue-group.js";
+export {default as musicRevenueWiggle} from "./music-revenue-wiggle.js";
export {default as ordinalBar} from "./ordinal-bar.js";
export {default as penguinAnnotated} from "./penguin-annotated.js";
export {default as penguinCulmen} from "./penguin-culmen.js";
@@ -142,12 +148,13 @@ export {default as penguinCulmenDelaunay} from "./penguin-culmen-delaunay.js";
export {default as penguinCulmenDelaunayMesh} from "./penguin-culmen-delaunay-mesh.js";
export {default as penguinCulmenDelaunaySpecies} from "./penguin-culmen-delaunay-species.js";
export {default as penguinCulmenVoronoi} from "./penguin-culmen-voronoi.js";
-export {default as penguinVoronoi1D} from "./penguin-voronoi-1d.js";
+export {default as penguinCumsumExclude} from "./penguin-cumsum-exclude.js";
export {default as penguinDensity} from "./penguin-density.js";
export {default as penguinDensityFill} from "./penguin-density-fill.js";
export {default as penguinDensityZ} from "./penguin-density-z.js";
export {default as penguinDodge} from "./penguin-dodge.js";
export {default as penguinDodgeHexbin} from "./penguin-dodge-hexbin.js";
+export {default as penguinDodgeReindexed} from "./penguin-dodge-reindexed.js";
export {default as penguinDodgeVoronoi} from "./penguin-dodge-voronoi.js";
export {default as penguinFacetDodge} from "./penguin-facet-dodge.js";
export {default as penguinFacetDodgeIdentity} from "./penguin-facet-dodge-identity.js";
@@ -167,6 +174,7 @@ export {default as penguinSpeciesGroup} from "./penguin-species-group.js";
export {default as penguinSpeciesIsland} from "./penguin-species-island.js";
export {default as penguinSpeciesIslandRelative} from "./penguin-species-island-relative.js";
export {default as penguinSpeciesIslandSex} from "./penguin-species-island-sex.js";
+export {default as penguinVoronoi1D} from "./penguin-voronoi-1d.js";
export {default as polylinear} from "./polylinear.js";
export {default as randomBins} from "./random-bins.js";
export {default as randomBinsXY} from "./random-bins-xy.js";
@@ -207,6 +215,7 @@ export {default as singleValueBar} from "./single-value-bar.js";
export {default as singleValueBin} from "./single-value-bin.js";
export {default as softwareVersions} from "./software-versions.js";
export {default as sparseCell} from "./sparse-cell.js";
+export {default as stackExclude} from "./stack-exclude.js";
export {default as stackedBar} from "./stacked-bar.js";
export {default as stackedRect} from "./stacked-rect.js";
export {default as stargazers} from "./stargazers.js";
diff --git a/test/plots/music-revenue-bars.js b/test/plots/music-revenue-bars.js
new file mode 100644
index 0000000000..e29eda1572
--- /dev/null
+++ b/test/plots/music-revenue-bars.js
@@ -0,0 +1,49 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType);
+ const stack = {x: (d) => d["year"].getFullYear(), y: "revenue", z: "format", order: "value", reverse: true};
+ return Plot.plot({
+ marginRight: 90,
+ marginBottom: 35,
+ facet: {data, y: "group", marginRight: 90},
+ x: {ticks: d3.range(1975, 2020, 5), tickFormat: ""},
+ y: {
+ grid: true,
+ label: "↑ Annual revenue (billions, adj.)",
+ transform: (d) => d / 1000,
+ nice: true
+ },
+ marks: [
+ Plot.frame(),
+ Plot.barY(
+ data,
+ Plot.groupX(
+ {y: "sum"},
+ Plot.windowY({
+ ...stack,
+ k: 3,
+ y: (d) => -d.revenue,
+ fill: "group",
+ facet: "exclude",
+ order: "sum"
+ })
+ )
+ ),
+ Plot.barY(
+ data,
+ Plot.groupX(
+ {y: "sum"},
+ Plot.windowY({
+ ...stack,
+ k: 3,
+ fill: "group",
+ title: (d) => `${d.format}\n${d.group}`
+ })
+ )
+ ),
+ Plot.ruleY([0])
+ ]
+ });
+}
diff --git a/test/plots/music-revenue-bin.js b/test/plots/music-revenue-bin.js
new file mode 100644
index 0000000000..adfa085c44
--- /dev/null
+++ b/test/plots/music-revenue-bin.js
@@ -0,0 +1,48 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType);
+ const stack = {x: "year", y: "revenue", z: "format", order: "value", reverse: true};
+ return Plot.plot({
+ marginRight: 90,
+ facet: {data, y: "group", marginRight: 90},
+ y: {
+ grid: true,
+ label: "↑ Annual revenue (billions, adj.)",
+ transform: (d) => d / 1000,
+ nice: true
+ },
+ marks: [
+ Plot.frame(),
+ Plot.rectY(
+ data,
+ Plot.binX(
+ {y: "sum"},
+ Plot.windowY({
+ ...stack,
+ k: 7,
+ interval: d3.utcYear.every(5),
+ y: (d) => -d.revenue,
+ fill: "#eee",
+ facet: "exclude"
+ })
+ )
+ ),
+ Plot.rectY(
+ data,
+ Plot.binX(
+ {y: "sum"},
+ Plot.windowY({
+ ...stack,
+ k: 7,
+ interval: d3.utcYear.every(5),
+ fill: "group",
+ title: (d) => `${d.format}\n${d.group}`
+ })
+ )
+ ),
+ Plot.ruleY([0])
+ ]
+ });
+}
diff --git a/test/plots/music-revenue-group.js b/test/plots/music-revenue-group.js
new file mode 100644
index 0000000000..5f0bcf9ac0
--- /dev/null
+++ b/test/plots/music-revenue-group.js
@@ -0,0 +1,35 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType);
+ const stack = {x: "year", y: "revenue", z: "format", order: "appearance", reverse: true};
+ return Plot.plot({
+ marginRight: 90,
+ facet: {data, y: "group", marginRight: 90},
+ y: {
+ grid: true,
+ label: "↑ Annual revenue (billions, adj.)",
+ transform: (d) => d / 1000
+ },
+ marks: [
+ Plot.areaY(data, Plot.stackY({...stack, fill: "group", title: (d) => `${d.format}\n${d.group}`})),
+ Plot.areaY(
+ data,
+ Plot.mapY(
+ (Y) => Y.map((d) => -d),
+ Plot.stackY({
+ ...stack,
+ y: "revenue",
+ fill: "#eee",
+ stroke: "#fff",
+ facet: "exclude"
+ })
+ )
+ ),
+ Plot.lineY(data, Plot.stackY2({...stack, stroke: "white", strokeWidth: 1})),
+ Plot.ruleY([0]),
+ Plot.frame()
+ ]
+ });
+}
diff --git a/test/plots/music-revenue-wiggle.js b/test/plots/music-revenue-wiggle.js
new file mode 100644
index 0000000000..0bee1b535c
--- /dev/null
+++ b/test/plots/music-revenue-wiggle.js
@@ -0,0 +1,40 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType);
+ const stack = {
+ x: "year",
+ y: "revenue",
+ z: "format",
+ // order and offset are used to test that these options follow facet reindexation
+ order: ["Cassette", "Paid Subscription"],
+ offset: "wiggle",
+ reverse: true
+ };
+ return Plot.plot({
+ marginRight: 90,
+ facet: {data, y: "group", marginRight: 90},
+ y: {
+ grid: true,
+ label: "↑ Annual revenue (billions, adj.)",
+ transform: (d) => d / 1000
+ },
+ marks: [
+ Plot.areaY(
+ data,
+ Plot.stackY({
+ ...stack,
+ y: (d) => -1 - d.revenue,
+ fill: "#eee",
+ stroke: "#fff",
+ facet: "exclude"
+ })
+ ),
+ Plot.areaY(data, Plot.stackY({...stack, fill: "group", title: (d) => `${d.format}\n${d.group}`})),
+ Plot.lineY(data, Plot.stackY2({...stack, stroke: "white", strokeWidth: 1})),
+ Plot.ruleY([0]),
+ Plot.frame()
+ ]
+ });
+}
diff --git a/test/plots/penguin-cumsum-exclude.js b/test/plots/penguin-cumsum-exclude.js
new file mode 100644
index 0000000000..d3f665a6be
--- /dev/null
+++ b/test/plots/penguin-cumsum-exclude.js
@@ -0,0 +1,37 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const penguins = await d3.csv("data/penguins.csv", d3.autoType);
+ return Plot.plot({
+ facet: {data: penguins, x: "island"},
+ width: 860,
+ height: 300,
+ y: {nice: true},
+ marks: [
+ Plot.frame(),
+ Plot.ruleY([0]),
+ Plot.dot(
+ penguins,
+ Plot.mapY(
+ "cumsum",
+ Plot.sort("body_mass_g", {x: "body_mass_g", y: -1, fill: "island", facet: "exclude", z: null, r: 1})
+ )
+ ),
+ Plot.lineY(
+ penguins,
+ Plot.mapY(
+ "cumsum",
+ Plot.sort("body_mass_g", {
+ x: "body_mass_g",
+ y: 1,
+ strokeWidth: 2,
+ stroke: "island",
+ facet: "include",
+ z: null
+ })
+ )
+ )
+ ]
+ });
+}
diff --git a/test/plots/penguin-dodge-reindexed.js b/test/plots/penguin-dodge-reindexed.js
new file mode 100644
index 0000000000..e4d7b76480
--- /dev/null
+++ b/test/plots/penguin-dodge-reindexed.js
@@ -0,0 +1,11 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const penguins = await d3.csv("data/penguins.csv", d3.autoType);
+ return Plot.plot({
+ facet: {data: penguins, y: "island"},
+ height: 400,
+ marks: [Plot.dot(penguins, Plot.dodgeY({x: "body_mass_g", facet: "exclude", fill: "island"}))]
+ });
+}
diff --git a/test/plots/stack-exclude.js b/test/plots/stack-exclude.js
new file mode 100644
index 0000000000..e2f2afdc81
--- /dev/null
+++ b/test/plots/stack-exclude.js
@@ -0,0 +1,26 @@
+import * as Plot from "@observablehq/plot";
+
+export default async function () {
+ const data = Float64Array.of(1, 2, 3);
+ const facets = ["a", "b", "c"];
+ return Plot.plot({
+ height: 180,
+ facet: {data, x: facets},
+ marks: [
+ Plot.barY(data, {
+ stroke: (d) => d, // channel as accessor
+ fill: data, // channel as array
+ fillOpacity: 0.5,
+ facet: "exclude"
+ }),
+ Plot.textY(
+ data,
+ Plot.stackY({
+ y: data,
+ text: (d, i) => i, // the original index
+ facet: "exclude"
+ })
+ )
+ ]
+ });
+}