diff --git a/src/marks/tip.js b/src/marks/tip.js
index bfb9d04cb2..718cab92f7 100644
--- a/src/marks/tip.js
+++ b/src/marks/tip.js
@@ -431,6 +431,8 @@ function* formatChannels(i, index, channels, scales, values) {
function formatPair(formatValue, c1, c2, i) {
return c2.hint?.length // e.g., stackY’s y1 and y2
? `${formatValue(c2.value[i] - c1.value[i], i)}`
+ : c2.hint?.single // e.g., waffleY’s y1 and y2
+ ? `${formatValue(c2.value[i], i)}`
: `${formatValue(c1.value[i], i)}–${formatValue(c2.value[i], i)}`;
}
diff --git a/src/marks/waffle.js b/src/marks/waffle.js
index c9d8771d21..0f4b173ba4 100644
--- a/src/marks/waffle.js
+++ b/src/marks/waffle.js
@@ -1,9 +1,11 @@
-import {extent, namespaces} from "d3";
+import {extent, namespaces, polygonCentroid} from "d3";
+import {valueObject} from "../channel.js";
import {create} from "../context.js";
import {composeRender} from "../mark.js";
import {hasXY, identity, indexOf} from "../options.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, getPatternId} from "../style.js";
import {template} from "../template.js";
+import {initializer} from "../transforms/basic.js";
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
@@ -14,8 +16,10 @@ const waffleDefaults = {
};
export class WaffleX extends BarX {
- constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) {
- super(data, {...options, render: composeRender(render, waffleRender("x"))}, waffleDefaults);
+ constructor(data, {unit = 1, gap = 1, round, render, multiple, tip, ...options} = {}) {
+ options = initializer({...options, render: composeRender(render, waffleRender("x"))}, waffleInitializer("x"));
+ if (tip) options = initializer({...options, tip}, waffleTipInitializer("x"));
+ super(data, options, waffleDefaults);
this.unit = Math.max(0, unit);
this.gap = +gap;
this.round = maybeRound(round);
@@ -24,8 +28,10 @@ export class WaffleX extends BarX {
}
export class WaffleY extends BarY {
- constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) {
- super(data, {...options, render: composeRender(render, waffleRender("y"))}, waffleDefaults);
+ constructor(data, {unit = 1, gap = 1, round, render, multiple, tip, ...options} = {}) {
+ options = initializer({...options, render: composeRender(render, waffleRender("y"))}, waffleInitializer("y"));
+ if (tip) options = initializer({...options, tip}, waffleTipInitializer("y"));
+ super(data, options, waffleDefaults);
this.unit = Math.max(0, unit);
this.gap = +gap;
this.round = maybeRound(round);
@@ -33,10 +39,11 @@ export class WaffleY extends BarY {
}
}
-function waffleRender(y) {
- return function (index, scales, values, dimensions, context) {
- const {unit, gap, rx, ry, round} = this;
- const {document} = context;
+function waffleInitializer(y) {
+ return function (data, facets, channels, scales, dimensions) {
+ const {round, unit} = this;
+
+ const values = valueObject(channels, scales);
const Y1 = values.channels[`${y}1`].value;
const Y2 = values.channels[`${y}2`].value;
@@ -56,9 +63,65 @@ function waffleRender(y) {
// TODO insets?
const transform = y === "y" ? ([x, y]) => [x * cx, -y * cy] : ([x, y]) => [y * cy, x * cx];
+ const P = Array.from(Y1, (_, i) => wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple).map(transform));
+
const tx = (barwidth - multiple * cx) / 2;
- const x0 = typeof barx === "function" ? (i) => barx(i) + tx : barx + tx;
- const y0 = scales[y](0);
+ this.x0 = typeof barx === "function" ? (i) => barx(i) + tx : barx + tx;
+ this.y0 = scales[y](0);
+ this.cx = cx;
+ this.cy = cy;
+ this.barwidth = barwidth;
+ this.barx = barx;
+ this.multiple = multiple;
+
+ return {channels: {polygon: {value: P, source: null}}};
+ };
+}
+
+function waffleTipInitializer(y) {
+ return function (data, facets, channels) {
+ const {x0, y0, barwidth} = this;
+ const P = channels.polygon.value;
+ const n = P.length;
+ const tx = typeof x0 === "function" ? (i) => x0(i) - barwidth / 2 : () => x0;
+ const ty = typeof y0 === "function" ? y0 : () => y0;
+
+ const X = new Float64Array(n);
+ const Y = new Float64Array(n);
+
+ const [ix, iy] = y === "y" ? [0, 1] : [1, 0];
+ for (let i = 0; i < n; ++i) {
+ const c = polygonCentroid(P[i]);
+ X[i] = c[ix] + tx(i);
+ Y[i] = c[iy] + ty(i);
+ }
+
+ // restore the tip value for y
+ const source = channels[`${y}2`].hint?.length
+ ? {
+ ...channels[`${y}1`],
+ value: Array.from(channels[`${y}1`].value, (d, i) => channels[`${y}2`].value[i] - d),
+ hint: {single: true}
+ }
+ : null;
+
+ const x = y === "y" ? "x" : "y";
+ return {
+ channels: {
+ [`${x}1`]: {value: X, scale: null, source: null},
+ [`${x}2`]: {value: X, scale: null, source: null},
+ [`${y}1`]: {value: Y, scale: null, source},
+ [`${y}2`]: {value: Y, scale: null, source}
+ }
+ };
+ };
+}
+
+function waffleRender(y) {
+ return function (index, scales, values, dimensions, context) {
+ const {gap, cx, cy, rx, ry, x0, y0} = this;
+ const {document} = context;
+ const polygon = values.channels.polygon.value;
// Create a base pattern with shared attributes for cloning.
const patternId = getPatternId();
@@ -95,13 +158,7 @@ function waffleRender(y) {
.enter()
.append("path")
.attr("transform", y === "y" ? template`translate(${x0},${y0})` : template`translate(${y0},${x0})`)
- .attr(
- "d",
- (i) =>
- `M${wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple)
- .map(transform)
- .join("L")}Z`
- )
+ .attr("d", (i) => `M${polygon[i].join("L")}Z`)
.attr("fill", (i) => `url(#${patternId}-${i})`)
.attr("stroke", this.stroke == null ? null : (i) => `url(#${patternId}-${i})`)
)
@@ -198,12 +255,12 @@ function spread(domain) {
return max - min;
}
-export function waffleX(data, options = {}) {
+export function waffleX(data, {tip, ...options} = {}) {
if (!hasXY(options)) options = {...options, y: indexOf, x2: identity};
- return new WaffleX(data, maybeStackX(maybeIntervalX(maybeIdentityX(options))));
+ return new WaffleX(data, {tip, ...maybeStackX(maybeIntervalX(maybeIdentityX(options)))});
}
-export function waffleY(data, options = {}) {
+export function waffleY(data, {tip, ...options} = {}) {
if (!hasXY(options)) options = {...options, x: indexOf, y2: identity};
- return new WaffleY(data, maybeStackY(maybeIntervalY(maybeIdentityY(options))));
+ return new WaffleY(data, {tip, ...maybeStackY(maybeIntervalY(maybeIdentityY(options)))});
}
diff --git a/test/output/waffleTip.svg b/test/output/waffleTip.svg
new file mode 100644
index 0000000000..c9925a71f1
--- /dev/null
+++ b/test/output/waffleTip.svg
@@ -0,0 +1,67 @@
+
\ No newline at end of file
diff --git a/test/output/waffleTipUnit.svg b/test/output/waffleTipUnit.svg
new file mode 100644
index 0000000000..2b8dfcc906
--- /dev/null
+++ b/test/output/waffleTipUnit.svg
@@ -0,0 +1,447 @@
+
\ No newline at end of file
diff --git a/test/output/waffleTipUnitX.svg b/test/output/waffleTipUnitX.svg
new file mode 100644
index 0000000000..e5e4a33dcc
--- /dev/null
+++ b/test/output/waffleTipUnitX.svg
@@ -0,0 +1,447 @@
+
\ No newline at end of file
diff --git a/test/output/waffleTipX.svg b/test/output/waffleTipX.svg
new file mode 100644
index 0000000000..4940dfa48b
--- /dev/null
+++ b/test/output/waffleTipX.svg
@@ -0,0 +1,70 @@
+
\ No newline at end of file
diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts
index 455efd3a4a..eff19a9c12 100644
--- a/test/plots/waffle.ts
+++ b/test/plots/waffle.ts
@@ -246,3 +246,36 @@ export async function waffleYGrouped() {
marks: [Plot.waffleY(athletes, Plot.groupX({y: "count"}, {x: "sport", unit: 10})), Plot.ruleY([0])]
});
}
+
+export function waffleTip() {
+ return Plot.plot({
+ color: {type: "sqrt", scheme: "spectral"},
+ y: {inset: 12},
+ marks: [Plot.waffleY([1, 4, 9, 24, 46, 66, 7], {x: null, fill: Plot.identity, tip: true})]
+ });
+}
+
+export function waffleTipUnit() {
+ return Plot.plot({
+ y: {inset: 12},
+ marks: [Plot.waffleY({length: 100}, {x: (d, i) => i % 3, y: 1, fill: d3.randomLcg(42), tip: true})]
+ });
+}
+
+export function waffleTipX() {
+ return Plot.plot({
+ style: {overflow: "visible"},
+ color: {type: "sqrt", scheme: "spectral"},
+ x: {label: "quantity"},
+ y: {inset: 12},
+ marks: [Plot.waffleX([1, 4, 9, 24, 46, 66, 7], {y: null, fill: Plot.identity, tip: true})]
+ });
+}
+
+export function waffleTipUnitX() {
+ return Plot.plot({
+ height: 300,
+ y: {inset: 12},
+ marks: [Plot.waffleX({length: 100}, {multiple: 5, y: (d, i) => i % 3, x: 1, fill: d3.randomLcg(42), tip: true})]
+ });
+}