From 7dcccea37cb0f2eef3fe03d5972cb488ded26107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sat, 10 Aug 2024 11:01:31 -0400 Subject: [PATCH 01/15] waffle tips --- src/marks/tip.js | 2 + src/marks/waffle.js | 101 ++++++-- test/output/waffleTip.svg | 67 +++++ test/output/waffleTipUnit.svg | 447 +++++++++++++++++++++++++++++++++ test/output/waffleTipUnitX.svg | 447 +++++++++++++++++++++++++++++++++ test/output/waffleTipX.svg | 70 ++++++ test/plots/waffle.ts | 33 +++ 7 files changed, 1145 insertions(+), 22 deletions(-) create mode 100644 test/output/waffleTip.svg create mode 100644 test/output/waffleTipUnit.svg create mode 100644 test/output/waffleTipUnitX.svg create mode 100644 test/output/waffleTipX.svg 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 @@ + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + 0 + 5 + 10 + 15 + 20 + 25 + 30 + + + + 0 + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + 0 + 1 + 2 + + + + 0 + 5 + 10 + 15 + 20 + 25 + 30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + + + quantity → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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})] + }); +} From da7d094342878bb5147914ab33588620c53f4203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 14 Aug 2024 18:42:21 +0200 Subject: [PATCH 02/15] test faceting --- test/output/waffleTipFacet.svg | 2084 +++++++++++++++++++++++++++++ test/output/waffleTipFacetX.svg | 2080 +++++++++++++++++++++++++++++ test/output/waffleTipFacetXY.svg | 2085 ++++++++++++++++++++++++++++++ test/plots/waffle.ts | 26 + 4 files changed, 6275 insertions(+) create mode 100644 test/output/waffleTipFacet.svg create mode 100644 test/output/waffleTipFacetX.svg create mode 100644 test/output/waffleTipFacetXY.svg diff --git a/test/output/waffleTipFacet.svg b/test/output/waffleTipFacet.svg new file mode 100644 index 0000000000..83e2abf8c8 --- /dev/null +++ b/test/output/waffleTipFacet.svg @@ -0,0 +1,2084 @@ + + + + + 0 + + + 1 + + + + + + 0 + 10 + 20 + 30 + 40 + 50 + 60 + 70 + 80 + + + + + + 0 + 1 + 2 + + + 0 + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleTipFacetX.svg b/test/output/waffleTipFacetX.svg new file mode 100644 index 0000000000..2fb0a28a3d --- /dev/null +++ b/test/output/waffleTipFacetX.svg @@ -0,0 +1,2080 @@ + + + + + 0 + + + 1 + + + + + + 0 + 1 + 2 + + + + + + 0 + 20 + 40 + 60 + 80 + + + 0 + 20 + 40 + 60 + 80 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleTipFacetXY.svg b/test/output/waffleTipFacetXY.svg new file mode 100644 index 0000000000..a7dda41ded --- /dev/null +++ b/test/output/waffleTipFacetXY.svg @@ -0,0 +1,2085 @@ + + + + + 0 + + + 1 + + + + + 0 + + + 1 + + + 2 + + + + + + 0 + 50 + + + 0 + 50 + + + 0 + 50 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts index eff19a9c12..6201e82fbd 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -262,6 +262,14 @@ export function waffleTipUnit() { }); } +export function waffleTipFacet() { + return Plot.plot({ + marks: [ + Plot.waffleY({length: 500}, {x: (d, i) => i % 3, fx: (d, i) => i % 2, y: 1, fill: d3.randomLcg(42), tip: true}) + ] + }); +} + export function waffleTipX() { return Plot.plot({ style: {overflow: "visible"}, @@ -279,3 +287,21 @@ export function waffleTipUnitX() { marks: [Plot.waffleX({length: 100}, {multiple: 5, y: (d, i) => i % 3, x: 1, fill: d3.randomLcg(42), tip: true})] }); } + +export function waffleTipFacetX() { + return Plot.plot({ + height: 500, + marks: [ + Plot.waffleX({length: 500}, {y: (d, i) => i % 3, fx: (d, i) => i % 2, x: 1, fill: d3.randomLcg(42), tip: true}) + ] + }); +} + +export function waffleTipFacetXY() { + return Plot.plot({ + height: 600, + marks: [ + Plot.waffleX({length: 500}, {fx: (d, i) => i % 3, fy: (d, i) => i % 2, x: 1, fill: d3.randomLcg(42), tip: true}) + ] + }); +} From 1f0d63469fbe3a0e5df7c445184b26c917ec2cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 15 Aug 2024 11:14:00 +0200 Subject: [PATCH 03/15] simpler x --- src/marks/waffle.js | 11 +++++------ test/plots/waffle.ts | 7 ++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 0f4b173ba4..5013117909 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -16,9 +16,9 @@ const waffleDefaults = { }; export class WaffleX extends BarX { - constructor(data, {unit = 1, gap = 1, round, render, multiple, tip, ...options} = {}) { + constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) { options = initializer({...options, render: composeRender(render, waffleRender("x"))}, waffleInitializer("x")); - if (tip) options = initializer({...options, tip}, waffleTipInitializer("x")); + if (options.tip) options = initializer(options, waffleTipInitializer("x")); super(data, options, waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; @@ -28,9 +28,9 @@ export class WaffleX extends BarX { } export class WaffleY extends BarY { - constructor(data, {unit = 1, gap = 1, round, render, multiple, tip, ...options} = {}) { + constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) { options = initializer({...options, render: composeRender(render, waffleRender("y"))}, waffleInitializer("y")); - if (tip) options = initializer({...options, tip}, waffleTipInitializer("y")); + if (options.tip) options = initializer(options, waffleTipInitializer("y")); super(data, options, waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; @@ -108,8 +108,7 @@ function waffleTipInitializer(y) { const x = y === "y" ? "x" : "y"; return { channels: { - [`${x}1`]: {value: X, scale: null, source: null}, - [`${x}2`]: {value: X, scale: null, source: null}, + [x]: {value: X, scale: null, source: null}, [`${y}1`]: {value: Y, scale: null, source}, [`${y}2`]: {value: Y, scale: null, source} } diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts index 6201e82fbd..8d5073ebd7 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -284,7 +284,12 @@ 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})] + marks: [ + Plot.waffleX( + {length: 100}, + {multiple: 5, y: (d, i) => i % 3, x: 1, fill: d3.randomLcg(42), tip: {format: {x: false}}} + ) + ] }); } From caa50093b3d60a5f0ed27aba8a269b372ff01981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 15 Aug 2024 18:43:33 +0200 Subject: [PATCH 04/15] support waffle pointer, and simplify --- src/marks/waffle.js | 50 ++-- test/output/wafflePointer.svg | 450 ++++++++++++++++++++++++++++++++++ test/plots/waffle.ts | 12 + 3 files changed, 482 insertions(+), 30 deletions(-) create mode 100644 test/output/wafflePointer.svg diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 5013117909..d5df9b02bf 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -18,7 +18,6 @@ const waffleDefaults = { export class WaffleX extends BarX { constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) { options = initializer({...options, render: composeRender(render, waffleRender("x"))}, waffleInitializer("x")); - if (options.tip) options = initializer(options, waffleTipInitializer("x")); super(data, options, waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; @@ -30,7 +29,6 @@ export class WaffleX extends BarX { export class WaffleY extends BarY { constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) { options = initializer({...options, render: composeRender(render, waffleRender("y"))}, waffleInitializer("y")); - if (options.tip) options = initializer(options, waffleTipInitializer("y")); super(data, options, waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; @@ -61,42 +59,34 @@ function waffleInitializer(y) { const cx = Math.min(barwidth / multiple, scale * multiple); const cy = scale * multiple; - // 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)); - + // The reference position. const tx = (barwidth - multiple * cx) / 2; - 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}}}; - }; -} + const x0 = typeof barx === "function" ? (i) => barx(i) + tx : barx + tx; + const y0 = scales[y](0); -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; + // TODO insets? + const transform = y === "y" ? ([x, y]) => [x * cx, -y * cy] : ([x, y]) => [y * cy, x * cx]; + const mx = typeof x0 === "function" ? (i) => x0(i) - barwidth / 2 : () => x0; + const [ix, iy] = y === "y" ? [0, 1] : [1, 0]; + const n = Y2.length; + const P = new Array(n); 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) { + P[i] = wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple).map(transform); const c = polygonCentroid(P[i]); - X[i] = c[ix] + tx(i); - Y[i] = c[iy] + ty(i); + X[i] = c[ix] + mx(i); + Y[i] = c[iy] + y0; } - // restore the tip value for y + this.cx = cx; + this.cy = cy; + this.x0 = x0; + this.y0 = y0; + + // Restore the tip value for y. const source = channels[`${y}2`].hint?.length ? { ...channels[`${y}1`], @@ -105,10 +95,10 @@ function waffleTipInitializer(y) { } : null; - const x = y === "y" ? "x" : "y"; return { channels: { - [x]: {value: X, scale: null, source: null}, + polygon: {value: P, source: null}, + [y === "y" ? "x" : "y"]: {value: X, scale: null, source: null}, [`${y}1`]: {value: Y, scale: null, source}, [`${y}2`]: {value: Y, scale: null, source} } diff --git a/test/output/wafflePointer.svg b/test/output/wafflePointer.svg new file mode 100644 index 0000000000..52bc316d01 --- /dev/null +++ b/test/output/wafflePointer.svg @@ -0,0 +1,450 @@ + + + + + 0 + 5 + 10 + 15 + 20 + 25 + 30 + + + + 0 + 1 + 2 + + + x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts index 8d5073ebd7..bb5cf3a9e0 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -247,6 +247,18 @@ export async function waffleYGrouped() { }); } +export function wafflePointer() { + const random = d3.randomLcg(42); + const data = Array.from({length: 100}, (_, i) => ({x: i % 3, fill: random()})); + return Plot.plot({ + y: {inset: 12}, + marks: [ + Plot.waffleY(data, {x: "x", y: 1, fill: "#888"}), + Plot.waffleY(data, Plot.pointer({x: "x", y: 1, fill: "fill"})) + ] + }); +} + export function waffleTip() { return Plot.plot({ color: {type: "sqrt", scheme: "spectral"}, From 83d0a4dd025646d9787f9ece9fb8d884a5c2904d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 29 Aug 2024 11:00:04 +0200 Subject: [PATCH 05/15] crazy logic to compute the waffles' centroids --- src/marks/waffle.js | 41 ++++++- test/output/wafflePointerFractional.svg | 136 ++++++++++++++++++++++++ test/plots/waffle.ts | 36 +++++++ 3 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 test/output/wafflePointerFractional.svg diff --git a/src/marks/waffle.js b/src/marks/waffle.js index d5df9b02bf..870e9d00dd 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -1,4 +1,4 @@ -import {extent, namespaces, polygonCentroid} from "d3"; +import {extent, namespaces} from "d3"; import {valueObject} from "../channel.js"; import {create} from "../context.js"; import {composeRender} from "../mark.js"; @@ -76,7 +76,7 @@ function waffleInitializer(y) { for (let i = 0; i < n; ++i) { P[i] = wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple).map(transform); - const c = polygonCentroid(P[i]); + const c = P[i].pop(); X[i] = c[ix] + mx(i); Y[i] = c[iy] + y0; } @@ -192,6 +192,8 @@ function waffleRender(y) { // Waffles can also represent fractional intervals (e.g., 2.4–10.1). These // require additional corner cuts, so the implementation below generates a few // more points. +// +// The last point describes the centroid (used for pointing) function wafflePoints(i1, i2, columns) { if (i1 < 0 || i2 < 0) { const k = Math.ceil(-Math.min(i1, i2) / columns); // shift negative to positive @@ -220,10 +222,43 @@ function wafflePoints(i1, i2, columns) { : [ [Math.floor(i2 % columns), Math.ceil(i2 / columns)], [0, Math.ceil(i2 / columns)] - ]) + ]), + centroid(i1, i2, columns) ]; } +function singleRowCentroid(i, j, columns) { + const c = Math.floor(j) - Math.floor(i); + return c === 0 // Single cell + ? [Math.floor(i % columns) + 0.5, Math.floor(i / columns) + (((i + j) / 2) % 1)] + : c === 1 // Two incomplete cells, use the overlap if it is large enough, otherwise use the largest + ? (j % 1) - (i % 1) > 0.5 + ? [Math.ceil(i % columns), Math.floor(j / columns) + ((i % 1) + (j % 1)) / 2] + : j % 1 > 1 - (i % 1) + ? [Math.floor(j % columns) + 0.5, Math.floor(j / columns) + (j % 1) / 2] + : [Math.floor(i % columns) + 0.5, Math.floor(i / columns) + (1 + (i % 1)) / 2] + : // At least one full cell, take their midpoint + [ + Math.ceil(i % columns) + Math.ceil(Math.floor(j) - Math.ceil(i)) / 2, + Math.floor(i / columns) + (j >= 1 + i ? 0.5 : ((i + j) / 2) % 1) + ]; +} + +function centroid(i1, i2, columns) { + const r = Math.floor(i2 / columns) - Math.floor(i1 / columns); + return r === 0 // Single row + ? singleRowCentroid(i1, i2, columns) + : // Two incomplete rows, use the midpoint of their overlap if they do, otherwise use the largest + r === 1 + ? Math.floor(i2 % columns) > Math.ceil(i1 % columns) + ? [(Math.floor(i2 % columns) + Math.ceil(i1 % columns)) / 2, Math.floor(i2 / columns)] + : i2 % columns > columns - (i1 % columns) + ? singleRowCentroid(i2 - (i2 % columns), i2, columns) + : singleRowCentroid(i1, columns * Math.ceil(i1 / columns), columns) + : // At least one full row, take the midpoint of all the rows that include the middle + [columns / 2, (Math.round(i1 / columns) + Math.round(i2 / columns)) / 2]; +} + function maybeRound(round) { if (round === undefined || round === false) return Number; if (round === true) return Math.round; diff --git a/test/output/wafflePointerFractional.svg b/test/output/wafflePointerFractional.svg new file mode 100644 index 0000000000..fd12792d0c --- /dev/null +++ b/test/output/wafflePointerFractional.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.51 + 0.99 + 0.5 + 6 + 0.3 + 1.6 + 9.1 + 2 + 18 + 6 + 0.5 + 2.5 + 46 + 34 + 20 + 7 + 0.5 + 0.1 + 0 + 2.5 + 1 + 0.1 + 0.8 + + + \ No newline at end of file diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts index bb5cf3a9e0..9d587756e9 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -1,5 +1,6 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; +import {svg} from "htl"; const demographics = d3.csvParse( `group,label,freq @@ -259,6 +260,41 @@ export function wafflePointer() { }); } +export function wafflePointerFractional() { + const values = [0.51, 0.99, 0.5, 6, 0.3, 1.6, 9.1, 2, 18, 6, 0.5, 2.5, 46, 34, 20, 7, 0.5, 0.1, 0, 2.5, 1, 0.1, 0.8]; + const multiple = 16; + return Plot.plot({ + axis: null, + y: {insetTop: 12}, + color: {scheme: "Dark2"}, + marks: [ + Plot.waffleY(values, { + x: null, + multiple, + fill: (d, i) => i % 7, + tip: true + }), + Plot.waffleY(values, { + x: null, + multiple, + // eslint-disable-next-line + render: (index, scales, values, dimensions, context, next) => { + const format = (d: number) => +d.toFixed(2); + const labels = (values.channels.y1 as any).source.value; + return svg`${Array.from( + index, + (i) => + svg`${format(labels[i])}` + )}`; + } + }) + ] + }); +} export function waffleTip() { return Plot.plot({ color: {type: "sqrt", scheme: "spectral"}, From f27e32163c6f93e00f4b7de3912799f0aecc5550 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 9 Sep 2024 08:30:00 -0700 Subject: [PATCH 06/15] remove single hint --- src/marks/tip.js | 2 -- src/marks/waffle.js | 13 ++----------- test/plots/waffle.ts | 5 +++-- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/marks/tip.js b/src/marks/tip.js index 718cab92f7..bfb9d04cb2 100644 --- a/src/marks/tip.js +++ b/src/marks/tip.js @@ -431,8 +431,6 @@ 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 870e9d00dd..2b8d07d6f5 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -86,21 +86,12 @@ function waffleInitializer(y) { this.x0 = x0; this.y0 = y0; - // 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; - return { channels: { polygon: {value: P, source: null}, [y === "y" ? "x" : "y"]: {value: X, scale: null, source: null}, - [`${y}1`]: {value: Y, scale: null, source}, - [`${y}2`]: {value: Y, scale: null, source} + [`${y}1`]: {value: Y, scale: null, source: channels[`${y}1`]}, + [`${y}2`]: {value: Y, scale: null, source: channels[`${y}2`]} } }; }; diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts index 9d587756e9..c30f69cc27 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -280,7 +280,8 @@ export function wafflePointerFractional() { // eslint-disable-next-line render: (index, scales, values, dimensions, context, next) => { const format = (d: number) => +d.toFixed(2); - const labels = (values.channels.y1 as any).source.value; + const y1 = (values.channels.y1 as any).source.value; + const y2 = (values.channels.y2 as any).source.value; return svg`${Array.from( index, (i) => @@ -288,7 +289,7 @@ export function wafflePointerFractional() { dy: "0.38em", x: values.x[i], y: values.y1[i] - }}>${format(labels[i])}` + }}>${format(y2[i] - y1[i])}` )}`; } }) From c7ef7263154fabe9658a6be661ef520ac9e6cbde Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 9 Sep 2024 08:38:53 -0700 Subject: [PATCH 07/15] prettier --- src/marks/waffle.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 2b8d07d6f5..c6eb4acc30 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -218,23 +218,6 @@ function wafflePoints(i1, i2, columns) { ]; } -function singleRowCentroid(i, j, columns) { - const c = Math.floor(j) - Math.floor(i); - return c === 0 // Single cell - ? [Math.floor(i % columns) + 0.5, Math.floor(i / columns) + (((i + j) / 2) % 1)] - : c === 1 // Two incomplete cells, use the overlap if it is large enough, otherwise use the largest - ? (j % 1) - (i % 1) > 0.5 - ? [Math.ceil(i % columns), Math.floor(j / columns) + ((i % 1) + (j % 1)) / 2] - : j % 1 > 1 - (i % 1) - ? [Math.floor(j % columns) + 0.5, Math.floor(j / columns) + (j % 1) / 2] - : [Math.floor(i % columns) + 0.5, Math.floor(i / columns) + (1 + (i % 1)) / 2] - : // At least one full cell, take their midpoint - [ - Math.ceil(i % columns) + Math.ceil(Math.floor(j) - Math.ceil(i)) / 2, - Math.floor(i / columns) + (j >= 1 + i ? 0.5 : ((i + j) / 2) % 1) - ]; -} - function centroid(i1, i2, columns) { const r = Math.floor(i2 / columns) - Math.floor(i1 / columns); return r === 0 // Single row @@ -250,6 +233,23 @@ function centroid(i1, i2, columns) { [columns / 2, (Math.round(i1 / columns) + Math.round(i2 / columns)) / 2]; } +function singleRowCentroid(i1, i2, columns) { + const c = Math.floor(i2) - Math.floor(i1); + return c === 0 // Single cell + ? [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (((i1 + i2) / 2) % 1)] + : c === 1 // Two incomplete cells, use the overlap if it is large enough, otherwise use the largest + ? (i2 % 1) - (i1 % 1) > 0.5 + ? [Math.ceil(i1 % columns), Math.floor(i2 / columns) + ((i1 % 1) + (i2 % 1)) / 2] + : i2 % 1 > 1 - (i1 % 1) + ? [Math.floor(i2 % columns) + 0.5, Math.floor(i2 / columns) + (i2 % 1) / 2] + : [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (1 + (i1 % 1)) / 2] + : // At least one full cell, take their midpoint + [ + Math.ceil(i1 % columns) + Math.ceil(Math.floor(i2) - Math.ceil(i1)) / 2, + Math.floor(i1 / columns) + (i2 >= 1 + i1 ? 0.5 : ((i1 + i2) / 2) % 1) + ]; +} + function maybeRound(round) { if (round === undefined || round === false) return Number; if (round === true) return Math.round; From 57f8e3c744770fd478ecf2390985bfc2a78206e1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 21 Nov 2024 09:00:43 -0800 Subject: [PATCH 08/15] tidy --- src/marks/waffle.js | 123 ++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index e7bef3043f..42d004cf57 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -16,9 +16,8 @@ const waffleDefaults = { }; export class WaffleX extends BarX { - constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) { - options = initializer({...options, render: composeRender(render, waffleRender("x"))}, waffleInitializer("x")); - super(data, options, waffleDefaults); + constructor(data, {unit = 1, gap = 1, round, multiple, ...options} = {}) { + super(data, wafflePolygon("x", options), waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; this.round = maybeRound(round); @@ -27,9 +26,8 @@ export class WaffleX extends BarX { } export class WaffleY extends BarY { - constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) { - options = initializer({...options, render: composeRender(render, waffleRender("y"))}, waffleInitializer("y")); - super(data, options, waffleDefaults); + constructor(data, {unit = 1, gap = 1, round, multiple, ...options} = {}) { + super(data, wafflePolygon("y", options), waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; this.round = maybeRound(round); @@ -37,8 +35,8 @@ export class WaffleY extends BarY { } } -function waffleInitializer(y) { - return function (data, facets, channels, scales, dimensions) { +function wafflePolygon(y, options) { + return initializer(waffleRender(y, options), function (data, facets, channels, scales, dimensions) { const {round, unit} = this; const values = valueObject(channels, scales); @@ -94,57 +92,60 @@ function waffleInitializer(y) { [`${y}2`]: {value: Y, scale: null, source: channels[`${y}2`]} } }; - }; + }); } -function waffleRender(y) { - return function (index, scales, values, dimensions, context) { - const {gap, cx, cy, rx, ry, x0, y0} = this; - const {ariaLabel, href, title, ...visualValues} = values; - const {document} = context; - const polygon = values.channels.polygon.value; +function waffleRender(y, {render, ...options}) { + return { + ...options, + render: composeRender(render, function (index, scales, values, dimensions, context) { + const {gap, cx, cy, rx, ry, x0, y0} = this; + const {ariaLabel, href, title, ...visualValues} = values; + const {document} = context; + const polygon = values.channels.polygon.value; - // Create a base pattern with shared attributes for cloning. - const patternId = getPatternId(); - const basePattern = document.createElementNS(namespaces.svg, "pattern"); - basePattern.setAttribute("width", y === "y" ? cx : cy); - basePattern.setAttribute("height", y === "y" ? cy : cx); - basePattern.setAttribute("patternUnits", "userSpaceOnUse"); - const basePatternRect = basePattern.appendChild(document.createElementNS(namespaces.svg, "rect")); - basePatternRect.setAttribute("x", gap / 2); - basePatternRect.setAttribute("y", gap / 2); - basePatternRect.setAttribute("width", (y === "y" ? cx : cy) - gap); - basePatternRect.setAttribute("height", (y === "y" ? cy : cx) - gap); - if (rx != null) basePatternRect.setAttribute("rx", rx); - if (ry != null) basePatternRect.setAttribute("ry", ry); + // Create a base pattern with shared attributes for cloning. + const patternId = getPatternId(); + const basePattern = document.createElementNS(namespaces.svg, "pattern"); + basePattern.setAttribute("width", y === "y" ? cx : cy); + basePattern.setAttribute("height", y === "y" ? cy : cx); + basePattern.setAttribute("patternUnits", "userSpaceOnUse"); + const basePatternRect = basePattern.appendChild(document.createElementNS(namespaces.svg, "rect")); + basePatternRect.setAttribute("x", gap / 2); + basePatternRect.setAttribute("y", gap / 2); + basePatternRect.setAttribute("width", (y === "y" ? cx : cy) - gap); + basePatternRect.setAttribute("height", (y === "y" ? cy : cx) - gap); + if (rx != null) basePatternRect.setAttribute("rx", rx); + if (ry != null) basePatternRect.setAttribute("ry", ry); - return create("svg:g", context) - .call(applyIndirectStyles, this, dimensions, context) - .call(this._transform, this, scales) - .call((g) => - g - .selectAll() - .data(index) - .enter() - .append(() => basePattern.cloneNode(true)) - .attr("id", (i) => `${patternId}-${i}`) - .select("rect") - .call(applyDirectStyles, this) - .call(applyChannelStyles, this, visualValues) - ) - .call((g) => - g - .selectAll() - .data(index) - .enter() - .append("path") - .attr("transform", y === "y" ? template`translate(${x0},${y0})` : template`translate(${y0},${x0})`) - .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})`) - .call(applyChannelStyles, this, {ariaLabel, href, title}) - ) - .node(); + return create("svg:g", context) + .call(applyIndirectStyles, this, dimensions, context) + .call(this._transform, this, scales) + .call((g) => + g + .selectAll() + .data(index) + .enter() + .append(() => basePattern.cloneNode(true)) + .attr("id", (i) => `${patternId}-${i}`) + .select("rect") + .call(applyDirectStyles, this) + .call(applyChannelStyles, this, visualValues) + ) + .call((g) => + g + .selectAll() + .data(index) + .enter() + .append("path") + .attr("transform", y === "y" ? template`translate(${x0},${y0})` : template`translate(${y0},${x0})`) + .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})`) + .call(applyChannelStyles, this, {ariaLabel, href, title}) + ) + .node(); + }) }; } @@ -216,26 +217,26 @@ function wafflePoints(i1, i2, columns) { points.push([x2f, y2c]); if (y2c > y1c) points.push([0, y2c]); } - points.push(centroid(i1, i2, columns)); + points.push(waffleCentroid(i1, i2, columns)); return points; } -function centroid(i1, i2, columns) { +function waffleCentroid(i1, i2, columns) { const r = Math.floor(i2 / columns) - Math.floor(i1 / columns); return r === 0 // Single row - ? singleRowCentroid(i1, i2, columns) + ? waffleRowCentroid(i1, i2, columns) : // Two incomplete rows, use the midpoint of their overlap if they do, otherwise use the largest r === 1 ? Math.floor(i2 % columns) > Math.ceil(i1 % columns) ? [(Math.floor(i2 % columns) + Math.ceil(i1 % columns)) / 2, Math.floor(i2 / columns)] : i2 % columns > columns - (i1 % columns) - ? singleRowCentroid(i2 - (i2 % columns), i2, columns) - : singleRowCentroid(i1, columns * Math.ceil(i1 / columns), columns) + ? waffleRowCentroid(i2 - (i2 % columns), i2, columns) + : waffleRowCentroid(i1, columns * Math.ceil(i1 / columns), columns) : // At least one full row, take the midpoint of all the rows that include the middle [columns / 2, (Math.round(i1 / columns) + Math.round(i2 / columns)) / 2]; } -function singleRowCentroid(i1, i2, columns) { +function waffleRowCentroid(i1, i2, columns) { const c = Math.floor(i2) - Math.floor(i1); return c === 0 // Single cell ? [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (((i1 + i2) / 2) % 1)] From 0c7224fc74d790a666045280d8d506c21277bfdf Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 21 Nov 2024 09:09:59 -0800 Subject: [PATCH 09/15] pass cell dimensions as channels --- src/marks/waffle.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 42d004cf57..4d457a1ade 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -36,7 +36,7 @@ export class WaffleY extends BarY { } function wafflePolygon(y, options) { - return initializer(waffleRender(y, options), function (data, facets, channels, scales, dimensions) { + return initializer(waffleRender(options), function (data, facets, channels, scales, dimensions) { const {round, unit} = this; const values = valueObject(channels, scales); @@ -74,19 +74,16 @@ function wafflePolygon(y, options) { for (let i = 0; i < n; ++i) { P[i] = wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple).map(transform); - const c = P[i].pop(); + const c = P[i].pop(); // TODO call waffleCentroid here instead? X[i] = c[ix] + mx(i); Y[i] = c[iy] + y0; } - this.cx = cx; - this.cy = cy; - this.x0 = x0; - this.y0 = y0; - return { channels: { polygon: {value: P, source: null}, + [y === "y" ? "cx" : "cy"]: {value: [cx, x0], source: null, filter: null}, + [y === "y" ? "cy" : "cx"]: {value: [cy, y0], source: null, filter: null}, [y === "y" ? "x" : "y"]: {value: X, scale: null, source: null}, [`${y}1`]: {value: Y, scale: null, source: channels[`${y}1`]}, [`${y}2`]: {value: Y, scale: null, source: channels[`${y}2`]} @@ -95,26 +92,28 @@ function wafflePolygon(y, options) { }); } -function waffleRender(y, {render, ...options}) { +function waffleRender({render, ...options}) { return { ...options, render: composeRender(render, function (index, scales, values, dimensions, context) { - const {gap, cx, cy, rx, ry, x0, y0} = this; - const {ariaLabel, href, title, ...visualValues} = values; + const {gap, rx, ry} = this; + const {channels, ariaLabel, href, title, ...visualValues} = values; const {document} = context; - const polygon = values.channels.polygon.value; + const polygon = channels.polygon.value; + const [cx, x0] = channels.cx.value; + const [cy, y0] = channels.cy.value; // Create a base pattern with shared attributes for cloning. const patternId = getPatternId(); const basePattern = document.createElementNS(namespaces.svg, "pattern"); - basePattern.setAttribute("width", y === "y" ? cx : cy); - basePattern.setAttribute("height", y === "y" ? cy : cx); + basePattern.setAttribute("width", cx); + basePattern.setAttribute("height", cy); basePattern.setAttribute("patternUnits", "userSpaceOnUse"); const basePatternRect = basePattern.appendChild(document.createElementNS(namespaces.svg, "rect")); basePatternRect.setAttribute("x", gap / 2); basePatternRect.setAttribute("y", gap / 2); - basePatternRect.setAttribute("width", (y === "y" ? cx : cy) - gap); - basePatternRect.setAttribute("height", (y === "y" ? cy : cx) - gap); + basePatternRect.setAttribute("width", cx - gap); + basePatternRect.setAttribute("height", cy - gap); if (rx != null) basePatternRect.setAttribute("rx", rx); if (ry != null) basePatternRect.setAttribute("ry", ry); @@ -138,7 +137,7 @@ function waffleRender(y, {render, ...options}) { .data(index) .enter() .append("path") - .attr("transform", y === "y" ? template`translate(${x0},${y0})` : template`translate(${y0},${x0})`) + .attr("transform", template`translate(${x0},${y0})`) .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})`) From e927e69bd0ae88200cc46c9f9c84094db4f69248 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 21 Nov 2024 09:19:19 -0800 Subject: [PATCH 10/15] prettier --- src/marks/waffle.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 4d457a1ade..c504f51886 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -148,7 +148,7 @@ function waffleRender({render, ...options}) { }; } -// A waffle is a approximately rectangular shape, but may have one or two corner +// A waffle is approximately a rectangular shape, but may have one or two corner // cuts if the starting or ending value is not an even multiple of the number of // columns (the width of the waffle in cells). We can represent any waffle by // 8 points; below is a waffle of five columns representing the interval 2–11: @@ -222,30 +222,33 @@ function wafflePoints(i1, i2, columns) { function waffleCentroid(i1, i2, columns) { const r = Math.floor(i2 / columns) - Math.floor(i1 / columns); - return r === 0 // Single row - ? waffleRowCentroid(i1, i2, columns) - : // Two incomplete rows, use the midpoint of their overlap if they do, otherwise use the largest - r === 1 - ? Math.floor(i2 % columns) > Math.ceil(i1 % columns) + return r === 0 + ? // Single row + waffleRowCentroid(i1, i2, columns) + : r === 1 + ? // Two incomplete rows; use the midpoint of their overlap if any, otherwise the larger row + Math.floor(i2 % columns) > Math.ceil(i1 % columns) ? [(Math.floor(i2 % columns) + Math.ceil(i1 % columns)) / 2, Math.floor(i2 / columns)] : i2 % columns > columns - (i1 % columns) ? waffleRowCentroid(i2 - (i2 % columns), i2, columns) : waffleRowCentroid(i1, columns * Math.ceil(i1 / columns), columns) - : // At least one full row, take the midpoint of all the rows that include the middle + : // At least one full row; take the midpoint of all the rows that include the middle [columns / 2, (Math.round(i1 / columns) + Math.round(i2 / columns)) / 2]; } function waffleRowCentroid(i1, i2, columns) { const c = Math.floor(i2) - Math.floor(i1); - return c === 0 // Single cell - ? [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (((i1 + i2) / 2) % 1)] - : c === 1 // Two incomplete cells, use the overlap if it is large enough, otherwise use the largest - ? (i2 % 1) - (i1 % 1) > 0.5 + return c === 0 + ? // Single cell + [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (((i1 + i2) / 2) % 1)] + : c === 1 + ? // Two incomplete cells; use the overlap if large enough, otherwise use the largest + (i2 % 1) - (i1 % 1) > 0.5 ? [Math.ceil(i1 % columns), Math.floor(i2 / columns) + ((i1 % 1) + (i2 % 1)) / 2] : i2 % 1 > 1 - (i1 % 1) ? [Math.floor(i2 % columns) + 0.5, Math.floor(i2 / columns) + (i2 % 1) / 2] : [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (1 + (i1 % 1)) / 2] - : // At least one full cell, take their midpoint + : // At least one full cell; take the midpoint [ Math.ceil(i1 % columns) + Math.ceil(Math.floor(i2) - Math.ceil(i1)) / 2, Math.floor(i1 / columns) + (i2 >= 1 + i1 ? 0.5 : ((i1 + i2) / 2) % 1) From 4ef503466c6e6c341fc19c0087fd9c9f25a8582c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 21 Nov 2024 09:20:12 -0800 Subject: [PATCH 11/15] no filter on polygon --- src/marks/waffle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index c504f51886..31246258ae 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -81,7 +81,7 @@ function wafflePolygon(y, options) { return { channels: { - polygon: {value: P, source: null}, + polygon: {value: P, source: null, filter: null}, [y === "y" ? "cx" : "cy"]: {value: [cx, x0], source: null, filter: null}, [y === "y" ? "cy" : "cx"]: {value: [cy, y0], source: null, filter: null}, [y === "y" ? "x" : "y"]: {value: X, scale: null, source: null}, From 1f4518c9a06222d59ec3d79110294be50db3e228 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 21 Nov 2024 09:34:17 -0800 Subject: [PATCH 12/15] prettier --- src/marks/waffle.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 31246258ae..7b278f8b64 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -74,7 +74,7 @@ function wafflePolygon(y, options) { for (let i = 0; i < n; ++i) { P[i] = wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple).map(transform); - const c = P[i].pop(); // TODO call waffleCentroid here instead? + const c = P[i].pop(); // extract the transformed centroid X[i] = c[ix] + mx(i); Y[i] = c[iy] + y0; } @@ -188,13 +188,8 @@ function waffleRender({render, ...options}) { // // The last point describes the centroid (used for pointing) function wafflePoints(i1, i2, columns) { - if (i1 < 0 || i2 < 0) { - const k = Math.ceil(-Math.min(i1, i2) / columns); // shift negative to positive - return wafflePoints(i1 + k * columns, i2 + k * columns, columns).map(([x, y]) => [x, y - k]); - } - if (i2 < i1) { - return wafflePoints(i2, i1, columns); - } + if (i2 < i1) return wafflePoints(i2, i1, columns); // ensure i1 <= i2 + if (i1 < 0) return wafflePointsOffset(i1, i2, columns, Math.ceil(-Math.min(i1, i2) / columns)); // ensure i1 >= 0 const x1f = Math.floor(i1 % columns); const x1c = Math.ceil(i1 % columns); const x2f = Math.floor(i2 % columns); @@ -220,6 +215,10 @@ function wafflePoints(i1, i2, columns) { return points; } +function wafflePointsOffset(i1, i2, columns, k) { + return wafflePoints(i1 + k * columns, i2 + k * columns, columns).map(([x, y]) => [x, y - k]); +} + function waffleCentroid(i1, i2, columns) { const r = Math.floor(i2 / columns) - Math.floor(i1 / columns); return r === 0 From 28bdcafe410adfedb4f209b37c9c7654a31f1bef Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 21 Nov 2024 09:40:51 -0800 Subject: [PATCH 13/15] fix tip option type --- src/mark.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mark.d.ts b/src/mark.d.ts index 4e5a60cbed..c40b1a1a56 100644 --- a/src/mark.d.ts +++ b/src/mark.d.ts @@ -1,6 +1,7 @@ import type {Channel, ChannelDomainSort, ChannelValue, ChannelValues, ChannelValueSpec} from "./channel.js"; import type {Context} from "./context.js"; import type {Dimensions} from "./dimensions.js"; +import type {PointerOptions} from "./interactions/pointer.js"; import type {TipOptions} from "./marks/tip.js"; import type {plot} from "./plot.js"; import type {ScaleFunctions} from "./scales.js"; @@ -288,7 +289,7 @@ export interface MarkOptions { title?: ChannelValue; /** Whether to generate a tooltip for this mark, and any tip options. */ - tip?: boolean | TipPointer | (TipOptions & {pointer?: TipPointer}); + tip?: boolean | TipPointer | (TipOptions & PointerOptions & {pointer?: TipPointer}); /** * How to clip the mark; one of: From bed5d25ad14018e476a62c67e67e0ea1557462ea Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 21 Nov 2024 09:58:58 -0800 Subject: [PATCH 14/15] fewer channels --- src/marks/waffle.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 7b278f8b64..1756d86317 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -36,16 +36,18 @@ export class WaffleY extends BarY { } function wafflePolygon(y, options) { + const x = y === "y" ? "x" : "y"; + const y1 = `${y}1`; + const y2 = `${y}2`; return initializer(waffleRender(options), 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; + const Y1 = channels[y1].value; + const Y2 = channels[y2].value; // We might not use all the available bandwidth if the cells don’t fit evenly. - const barwidth = this[y === "y" ? "_width" : "_height"](scales, values, dimensions); - const barx = this[y === "y" ? "_x" : "_y"](scales, values, dimensions); + const xy = valueObject({...(x in channels && {[x]: channels[x]}), [y1]: channels[y1], [y2]: channels[y2]}, scales); + const barwidth = this[y === "y" ? "_width" : "_height"](scales, xy, dimensions); + const barx = this[y === "y" ? "_x" : "_y"](scales, xy, dimensions); // The length of a unit along y in pixels. const scale = unit * scaleof(scales.scales[y]); @@ -82,11 +84,11 @@ function wafflePolygon(y, options) { return { channels: { polygon: {value: P, source: null, filter: null}, - [y === "y" ? "cx" : "cy"]: {value: [cx, x0], source: null, filter: null}, - [y === "y" ? "cy" : "cx"]: {value: [cy, y0], source: null, filter: null}, - [y === "y" ? "x" : "y"]: {value: X, scale: null, source: null}, - [`${y}1`]: {value: Y, scale: null, source: channels[`${y}1`]}, - [`${y}2`]: {value: Y, scale: null, source: channels[`${y}2`]} + [`c${x}`]: {value: [cx, x0], source: null, filter: null}, + [`c${y}`]: {value: [cy, y0], source: null, filter: null}, + [x]: {value: X, scale: null, source: null}, + [y1]: {value: Y, scale: null, source: channels[y1]}, + [y2]: {value: Y, scale: null, source: channels[y2]} } }; }); From a67bd8d9a438368042dda4ad02bb9d9dc14762a7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 21 Nov 2024 11:22:50 -0800 Subject: [PATCH 15/15] default maxRadius to infinity --- src/marks/waffle.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 1756d86317..3b2b6d5774 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -2,7 +2,7 @@ import {extent, namespaces} 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 {hasXY, identity, indexOf, isObject} from "../options.js"; import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, getPatternId} from "../style.js"; import {template} from "../template.js"; import {initializer} from "../transforms/basic.js"; @@ -278,10 +278,26 @@ function spread(domain) { export function waffleX(data, {tip, ...options} = {}) { if (!hasXY(options)) options = {...options, y: indexOf, x2: identity}; - return new WaffleX(data, {tip, ...maybeStackX(maybeIntervalX(maybeIdentityX(options)))}); + return new WaffleX(data, {tip: waffleTip(tip), ...maybeStackX(maybeIntervalX(maybeIdentityX(options)))}); } export function waffleY(data, {tip, ...options} = {}) { if (!hasXY(options)) options = {...options, x: indexOf, y2: identity}; - return new WaffleY(data, {tip, ...maybeStackY(maybeIntervalY(maybeIdentityY(options)))}); + return new WaffleY(data, {tip: waffleTip(tip), ...maybeStackY(maybeIntervalY(maybeIdentityY(options)))}); +} + +/** + * Waffle tips behave a bit unpredictably because we they are driven by the + * waffle centroid; you could be hovering over a waffle segment, but more than + * 40px away from its centroid, or closer to the centroid of another segment. + * We’d rather show a tip, even if it’s the “wrong” one, so we increase the + * default maxRadius to Infinity. The “right” way to fix this would be to use + * signed distance to the waffle geometry rather than the centroid. + */ +function waffleTip(tip) { + return tip === true + ? {maxRadius: Infinity} + : isObject(tip) && tip.maxRadius === undefined + ? {...tip, maxRadius: Infinity} + : undefined; }