Skip to content

Commit 7dcccea

Browse files
committed
waffle tips
1 parent 1d01e25 commit 7dcccea

File tree

7 files changed

+1145
-22
lines changed

7 files changed

+1145
-22
lines changed

src/marks/tip.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ function* formatChannels(i, index, channels, scales, values) {
431431
function formatPair(formatValue, c1, c2, i) {
432432
return c2.hint?.length // e.g., stackY’s y1 and y2
433433
? `${formatValue(c2.value[i] - c1.value[i], i)}`
434+
: c2.hint?.single // e.g., waffleY’s y1 and y2
435+
? `${formatValue(c2.value[i], i)}`
434436
: `${formatValue(c1.value[i], i)}${formatValue(c2.value[i], i)}`;
435437
}
436438

src/marks/waffle.js

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import {extent, namespaces} from "d3";
1+
import {extent, namespaces, polygonCentroid} from "d3";
2+
import {valueObject} from "../channel.js";
23
import {create} from "../context.js";
34
import {composeRender} from "../mark.js";
45
import {hasXY, identity, indexOf} from "../options.js";
56
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, getPatternId} from "../style.js";
67
import {template} from "../template.js";
8+
import {initializer} from "../transforms/basic.js";
79
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
810
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
911
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
@@ -14,8 +16,10 @@ const waffleDefaults = {
1416
};
1517

1618
export class WaffleX extends BarX {
17-
constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) {
18-
super(data, {...options, render: composeRender(render, waffleRender("x"))}, waffleDefaults);
19+
constructor(data, {unit = 1, gap = 1, round, render, multiple, tip, ...options} = {}) {
20+
options = initializer({...options, render: composeRender(render, waffleRender("x"))}, waffleInitializer("x"));
21+
if (tip) options = initializer({...options, tip}, waffleTipInitializer("x"));
22+
super(data, options, waffleDefaults);
1923
this.unit = Math.max(0, unit);
2024
this.gap = +gap;
2125
this.round = maybeRound(round);
@@ -24,19 +28,22 @@ export class WaffleX extends BarX {
2428
}
2529

2630
export class WaffleY extends BarY {
27-
constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) {
28-
super(data, {...options, render: composeRender(render, waffleRender("y"))}, waffleDefaults);
31+
constructor(data, {unit = 1, gap = 1, round, render, multiple, tip, ...options} = {}) {
32+
options = initializer({...options, render: composeRender(render, waffleRender("y"))}, waffleInitializer("y"));
33+
if (tip) options = initializer({...options, tip}, waffleTipInitializer("y"));
34+
super(data, options, waffleDefaults);
2935
this.unit = Math.max(0, unit);
3036
this.gap = +gap;
3137
this.round = maybeRound(round);
3238
this.multiple = maybeMultiple(multiple);
3339
}
3440
}
3541

36-
function waffleRender(y) {
37-
return function (index, scales, values, dimensions, context) {
38-
const {unit, gap, rx, ry, round} = this;
39-
const {document} = context;
42+
function waffleInitializer(y) {
43+
return function (data, facets, channels, scales, dimensions) {
44+
const {round, unit} = this;
45+
46+
const values = valueObject(channels, scales);
4047
const Y1 = values.channels[`${y}1`].value;
4148
const Y2 = values.channels[`${y}2`].value;
4249

@@ -56,9 +63,65 @@ function waffleRender(y) {
5663

5764
// TODO insets?
5865
const transform = y === "y" ? ([x, y]) => [x * cx, -y * cy] : ([x, y]) => [y * cy, x * cx];
66+
const P = Array.from(Y1, (_, i) => wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple).map(transform));
67+
5968
const tx = (barwidth - multiple * cx) / 2;
60-
const x0 = typeof barx === "function" ? (i) => barx(i) + tx : barx + tx;
61-
const y0 = scales[y](0);
69+
this.x0 = typeof barx === "function" ? (i) => barx(i) + tx : barx + tx;
70+
this.y0 = scales[y](0);
71+
this.cx = cx;
72+
this.cy = cy;
73+
this.barwidth = barwidth;
74+
this.barx = barx;
75+
this.multiple = multiple;
76+
77+
return {channels: {polygon: {value: P, source: null}}};
78+
};
79+
}
80+
81+
function waffleTipInitializer(y) {
82+
return function (data, facets, channels) {
83+
const {x0, y0, barwidth} = this;
84+
const P = channels.polygon.value;
85+
const n = P.length;
86+
const tx = typeof x0 === "function" ? (i) => x0(i) - barwidth / 2 : () => x0;
87+
const ty = typeof y0 === "function" ? y0 : () => y0;
88+
89+
const X = new Float64Array(n);
90+
const Y = new Float64Array(n);
91+
92+
const [ix, iy] = y === "y" ? [0, 1] : [1, 0];
93+
for (let i = 0; i < n; ++i) {
94+
const c = polygonCentroid(P[i]);
95+
X[i] = c[ix] + tx(i);
96+
Y[i] = c[iy] + ty(i);
97+
}
98+
99+
// restore the tip value for y
100+
const source = channels[`${y}2`].hint?.length
101+
? {
102+
...channels[`${y}1`],
103+
value: Array.from(channels[`${y}1`].value, (d, i) => channels[`${y}2`].value[i] - d),
104+
hint: {single: true}
105+
}
106+
: null;
107+
108+
const x = y === "y" ? "x" : "y";
109+
return {
110+
channels: {
111+
[`${x}1`]: {value: X, scale: null, source: null},
112+
[`${x}2`]: {value: X, scale: null, source: null},
113+
[`${y}1`]: {value: Y, scale: null, source},
114+
[`${y}2`]: {value: Y, scale: null, source}
115+
}
116+
};
117+
};
118+
}
119+
120+
function waffleRender(y) {
121+
return function (index, scales, values, dimensions, context) {
122+
const {gap, cx, cy, rx, ry, x0, y0} = this;
123+
const {document} = context;
124+
const polygon = values.channels.polygon.value;
62125

63126
// Create a base pattern with shared attributes for cloning.
64127
const patternId = getPatternId();
@@ -95,13 +158,7 @@ function waffleRender(y) {
95158
.enter()
96159
.append("path")
97160
.attr("transform", y === "y" ? template`translate(${x0},${y0})` : template`translate(${y0},${x0})`)
98-
.attr(
99-
"d",
100-
(i) =>
101-
`M${wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple)
102-
.map(transform)
103-
.join("L")}Z`
104-
)
161+
.attr("d", (i) => `M${polygon[i].join("L")}Z`)
105162
.attr("fill", (i) => `url(#${patternId}-${i})`)
106163
.attr("stroke", this.stroke == null ? null : (i) => `url(#${patternId}-${i})`)
107164
)
@@ -198,12 +255,12 @@ function spread(domain) {
198255
return max - min;
199256
}
200257

201-
export function waffleX(data, options = {}) {
258+
export function waffleX(data, {tip, ...options} = {}) {
202259
if (!hasXY(options)) options = {...options, y: indexOf, x2: identity};
203-
return new WaffleX(data, maybeStackX(maybeIntervalX(maybeIdentityX(options))));
260+
return new WaffleX(data, {tip, ...maybeStackX(maybeIntervalX(maybeIdentityX(options)))});
204261
}
205262

206-
export function waffleY(data, options = {}) {
263+
export function waffleY(data, {tip, ...options} = {}) {
207264
if (!hasXY(options)) options = {...options, x: indexOf, y2: identity};
208-
return new WaffleY(data, maybeStackY(maybeIntervalY(maybeIdentityY(options))));
265+
return new WaffleY(data, {tip, ...maybeStackY(maybeIntervalY(maybeIdentityY(options)))});
209266
}

test/output/waffleTip.svg

Lines changed: 67 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)