From 819e3f299eafed2c72fcf7bd61f9e6ffa8878198 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] waffle tips --- src/marks/waffle.js | 80 ++++-- test/output/waffleTip.svg | 67 +++++ test/output/waffleTipUnit.svg | 447 ++++++++++++++++++++++++++++++++++ test/plots/waffle.ts | 15 ++ 4 files changed, 593 insertions(+), 16 deletions(-) create mode 100644 test/output/waffleTip.svg create mode 100644 test/output/waffleTipUnit.svg diff --git a/src/marks/waffle.js b/src/marks/waffle.js index c9d8771d21..482146b31a 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -1,4 +1,4 @@ -import {extent, namespaces} from "d3"; +import {extent, namespaces, polygonCentroid} from "d3"; import {create} from "../context.js"; import {composeRender} from "../mark.js"; import {hasXY, identity, indexOf} from "../options.js"; @@ -8,6 +8,7 @@ import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js"; import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; import {BarX, BarY} from "./bar.js"; +import {initializer} from "../transforms/basic.js"; const waffleDefaults = { ariaLabel: "waffle" @@ -15,7 +16,28 @@ 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); + super( + data, + initializer({...options, render: composeRender(render, waffleRender("x"))}, function (data, facets, channels) { + const n = channels.x1.value.length; + const X = new Float64Array(n); + const Y = new Float64Array(n); + return { + data, + facets, + channels: { + ...channels, + x1: {value: X, scale: null, source: null}, + x2: {value: X, scale: null, source: null}, + y1: {value: Y, scale: null, source: null}, + y2: {value: Y, scale: null, source: null}, + s1: {...channels.x1, source: null}, + s2: {...channels.x2, source: null} + } + }; + }), + waffleDefaults + ); this.unit = Math.max(0, unit); this.gap = +gap; this.round = maybeRound(round); @@ -25,7 +47,28 @@ 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); + super( + data, + initializer({...options, render: composeRender(render, waffleRender("y"))}, function (data, facets, channels) { + const n = channels.y1.value.length; + const X = new Float64Array(n); + const Y = new Float64Array(n); + return { + data, + facets, + channels: { + ...channels, + x1: {value: X, scale: null, source: null}, + x2: {value: X, scale: null, source: null}, + y1: {value: Y, scale: null, source: null}, + y2: {value: Y, scale: null, source: null}, + s1: {...channels.y1, source: null}, + s2: {...channels.y2, source: null} + } + }; + }), + waffleDefaults + ); this.unit = Math.max(0, unit); this.gap = +gap; this.round = maybeRound(round); @@ -37,8 +80,8 @@ function waffleRender(y) { return function (index, scales, values, dimensions, context) { const {unit, gap, rx, ry, round} = this; const {document} = context; - const Y1 = values.channels[`${y}1`].value; - const Y2 = values.channels[`${y}2`].value; + const Y1 = values.channels["s1"].value; + const Y2 = values.channels["s2"].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); @@ -74,6 +117,9 @@ function waffleRender(y) { if (rx != null) basePatternRect.setAttribute("rx", rx); if (ry != null) basePatternRect.setAttribute("ry", ry); + const X = values.channels.x1.value; + const Y = values.channels.y1.value; + return create("svg:g", context) .call(applyIndirectStyles, this, dimensions, context) .call(this._transform, this, scales) @@ -95,13 +141,13 @@ 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) => { + const pts = wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple).map(transform); + const [xa, ya] = polygonCentroid(pts); + X[i] = xa + (typeof x0 === "function" ? x0(i) - barwidth / 2 : x0); + Y[i] = ya + (typeof y0 === "function" ? y0(i) : y0); + return `M${pts.join("L")}Z`; + }) .attr("fill", (i) => `url(#${patternId}-${i})`) .attr("stroke", this.stroke == null ? null : (i) => `url(#${patternId}-${i})`) ) @@ -198,12 +244,14 @@ function spread(domain) { return max - min; } -export function waffleX(data, options = {}) { +export function waffleX(data, {tip, ...options} = {}) { + if (tip === true) tip = "xy"; if (!hasXY(options)) options = {...options, y: indexOf, x2: identity}; - return new WaffleX(data, maybeStackX(maybeIntervalX(maybeIdentityX(options)))); + return new WaffleX(data, maybeStackX(maybeIntervalX(maybeIdentityX({...options, tip})))); } -export function waffleY(data, options = {}) { +export function waffleY(data, {tip, ...options} = {}) { + if (tip === true) tip = "xy"; if (!hasXY(options)) options = {...options, x: indexOf, y2: identity}; - return new WaffleY(data, maybeStackY(maybeIntervalY(maybeIdentityY(options)))); + return new WaffleY(data, maybeStackY(maybeIntervalY(maybeIdentityY({...options, tip})))); } 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/plots/waffle.ts b/test/plots/waffle.ts index 455efd3a4a..7832f30959 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -246,3 +246,18 @@ 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})] + }); +}