From e5adcfe8178cdd1868e4772e0112798d1ce94f42 Mon Sep 17 00:00:00 2001 From: Mike Stringer Date: Thu, 14 Nov 2024 20:04:54 -0600 Subject: [PATCH 1/7] fixed typo (incorrect return value) in features/interval documentation (#2227) --- docs/features/intervals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/intervals.md b/docs/features/intervals.md index f631ecdea0..a672c9e71a 100644 --- a/docs/features/intervals.md +++ b/docs/features/intervals.md @@ -21,7 +21,7 @@ The *interval*.**offset** method takes a *value* and returns the corresponding v ```js Plot.utcInterval("day").offset(new Date("2013-04-12T12:34:56Z"), 1) // 2013-04-13T12:34:56Z -Plot.utcInterval("day").offset(new Date("2013-04-12T12:34:56Z"), -2) // 2013-03-22T12:34:56Z +Plot.utcInterval("day").offset(new Date("2013-04-12T12:34:56Z"), -2) // 2013-04-10T12:34:56Z ``` The *interval*.**range** method returns an array of values representing every interval boundary greater than or equal to *start* (inclusive) and less than *stop* (exclusive). The first value in the returned array is the least boundary greater than or equal to *start*; subsequent values are offset by intervals and floored. From 419232bcc2d0d010851eead2420f771be32c2e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 15 Nov 2024 03:12:22 +0100 Subject: [PATCH 2/7] Fix facet positioning of a rendered rendered SVGSVGElement (#2219) * Wrap any rendered SVGSVGElement in a SVGGElement closes #2218 * use x, y instead of a wrapper (review suggestion) * inline template --------- Co-authored-by: Mike Bostock --- docs/features/marks.md | 2 +- src/facet.js | 15 +- src/plot.js | 2 +- test/output/nestedFacets.html | 1008 +++++++++++++++++++++++++++++++++ test/plots/index.ts | 1 + test/plots/nested-facets.ts | 53 ++ 6 files changed, 1074 insertions(+), 7 deletions(-) create mode 100644 test/output/nestedFacets.html create mode 100644 test/plots/nested-facets.ts diff --git a/docs/features/marks.md b/docs/features/marks.md index 4d9cb90cd4..7e163907e6 100644 --- a/docs/features/marks.md +++ b/docs/features/marks.md @@ -114,7 +114,7 @@ Plot.plot({ ``` ::: -Marks may also be a function which returns an SVG element, if you wish to insert arbitrary content. (Here we use [Hypertext Literal](https://github.com/observablehq/htl) to generate an SVG gradient.) +Marks may also be a function which returns an [SVG element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element), if you wish to insert arbitrary content. (Here we use [Hypertext Literal](https://github.com/observablehq/htl) to generate an SVG gradient.) :::plot defer https://observablehq.com/@observablehq/plot-gradient-bars ```js diff --git a/src/facet.js b/src/facet.js index 5398b344e7..cc3af59ac4 100644 --- a/src/facet.js +++ b/src/facet.js @@ -63,11 +63,16 @@ export function facetGroups(data, {fx, fy}) { } export function facetTranslator(fx, fy, {marginTop, marginLeft}) { - return fx && fy - ? ({x, y}) => `translate(${fx(x) - marginLeft},${fy(y) - marginTop})` - : fx - ? ({x}) => `translate(${fx(x) - marginLeft},0)` - : ({y}) => `translate(0,${fy(y) - marginTop})`; + const x = fx ? ({x}) => fx(x) - marginLeft : () => 0; + const y = fy ? ({y}) => fy(y) - marginTop : () => 0; + return function (d) { + if (this.tagName === "svg") { + this.setAttribute("x", x(d)); + this.setAttribute("y", y(d)); + } else { + this.setAttribute("transform", `translate(${x(d)},${y(d)})`); + } + }; } // Returns an index that for each facet lists all the elements present in other diff --git a/src/plot.js b/src/plot.js index a7383a66c7..829c4624a4 100644 --- a/src/plot.js +++ b/src/plot.js @@ -317,7 +317,7 @@ export function plot(options = {}) { } } } - g?.selectChildren().attr("transform", facetTranslate); + g?.selectChildren().each(facetTranslate); } } diff --git a/test/output/nestedFacets.html b/test/output/nestedFacets.html new file mode 100644 index 0000000000..78280a3cce --- /dev/null +++ b/test/output/nestedFacets.html @@ -0,0 +1,1008 @@ +
+ + + + + + + + + + IF + + + + SI1 + + + + I1 + + + clarity + + + + + + 52 + 54 + 56 + 58 + 60 + 62 + 64 + 66 + 68 + 70 + + + + depth → + + + + D + + + E + + + F + + + + color + + + + + + + + + + + Fair + + + Good + + + Ideal + + + Premium + + + Very Good + + + + cut + + + + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + + clarity + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fair + + + Good + + + Ideal + + + Premium + + + Very Good + + + + cut + + + + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fair + + + Good + + + Ideal + + + Premium + + + Very Good + + + + cut + + + + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + IF + SI1 + I1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/plots/index.ts b/test/plots/index.ts index b13d1ec45b..3176991d75 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -184,6 +184,7 @@ export * from "./movies-profit-by-genre.js"; export * from "./movies-rating-by-genre.js"; export * from "./multiplication-table.js"; export * from "./music-revenue.js"; +export * from "./nested-facets.js"; export * from "./npm-versions.js"; export * from "./opacity.js"; export * from "./ordinal-bar.js"; diff --git a/test/plots/nested-facets.ts b/test/plots/nested-facets.ts new file mode 100644 index 0000000000..fa952a4464 --- /dev/null +++ b/test/plots/nested-facets.ts @@ -0,0 +1,53 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function nestedFacets() { + const diamonds = await d3.csv("data/diamonds.csv", d3.autoType); + return Plot.plot({ + width: 960, + height: 480, + fx: {domain: ["D", "E", "F"]}, + color: {legend: "ramp", domain: ["IF", "SI1", "I1"]}, + y: {domain: [51, 71.9], insetTop: 20, labelAnchor: "center"}, + marginLeft: 40, + marginBottom: 40, + marginTop: 35, + marks: [ + Plot.axisFx({anchor: "top"}), + Plot.frame({anchor: "top", strokeOpacity: 1}), + Plot.dot(diamonds, { + fx: "color", // outer x facet + y: "depth", // shared y scale + fill: "clarity", // shared color scale + render(index, {scales}, _values, {facet, ...dimensions}) { + const data = Array.from(index, (i) => this.data[i]); // subplot dataset as a subset of the data + return Plot.plot({ + ...dimensions, + marginTop: 60, + ...scales, // shared color scale, shared y scale + fx: {axis: "bottom", paddingOuter: 0.1, paddingInner: 0.2}, // inner x facet + x: { + domain: scales.color.domain, + axis: "top", + labelAnchor: "left", + labelOffset: 16, + ...(index["fi"] && {label: null}), + grid: true, + tickSize: 0 + }, // new x scale with a common domain and additional axis options + y: {...scales.y, grid: 4, axis: null}, // shared y scale with additional options + marks: [ + Plot.frame({anchor: "bottom"}), + Plot.boxY(data, { + fx: "cut", + x: "clarity", + y: "depth", + fill: "clarity" + }) + ] + }) as SVGElement; + } + }) + ] + }); +} From 653af6260b26de692ed41356ca3233075e884005 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 14 Nov 2024 23:43:27 -0800 Subject: [PATCH 3/7] remove module-alias (#2235) --- package.json | 4 ---- yarn.lock | 5 ----- 2 files changed, 9 deletions(-) diff --git a/package.json b/package.json index b0126d24f4..f493bcb9bb 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,6 @@ "docs:build": "node --experimental-network-imports node_modules/vitepress/dist/node/cli.js build docs", "docs:preview": "vitepress preview docs" }, - "_moduleAliases": { - "@observablehq/plot": "./src/index.js" - }, "sideEffects": [ "./src/index.js" ], @@ -67,7 +64,6 @@ "jsdom": "^24.0.0", "markdown-it-container": "^4.0.0", "mocha": "^10.0.0", - "module-alias": "^2.0.0", "prettier": "~3.0.0", "rollup": "^4.9.1", "topojson-client": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index 384c7783c2..4d03086e4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2853,11 +2853,6 @@ mocha@^10.0.0: yargs-parser "^20.2.9" yargs-unparser "^2.0.0" -module-alias@^2.0.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.3.tgz#ec2e85c68973bda6ab71ce7c93b763ec96053221" - integrity sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" From ff7d83cf74e9b9e7392c982196d76480fe33afa7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 14 Nov 2024 23:45:16 -0800 Subject: [PATCH 4/7] fix tickFormat type inference for empty domain (#2232) --- src/marks/axis.js | 4 ++-- test/output/tickFormatEmptyDomain.svg | 17 +++++++++++++++++ test/output/tickFormatEmptyFacetDomain.svg | 17 +++++++++++++++++ test/plots/index.ts | 1 + test/plots/tick-format.ts | 9 +++++++++ 5 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 test/output/tickFormatEmptyDomain.svg create mode 100644 test/output/tickFormatEmptyFacetDomain.svg create mode 100644 test/plots/tick-format.ts diff --git a/src/marks/axis.js b/src/marks/axis.js index 7c7944a55f..3a0e644909 100644 --- a/src/marks/axis.js +++ b/src/marks/axis.js @@ -672,10 +672,10 @@ export function inferTickFormat(scale, data, ticks, tickFormat, anchor) { ? inferTimeFormat(scale.type, data, anchor) ?? formatDefault : scale.tickFormat ? scale.tickFormat(typeof ticks === "number" ? ticks : null, tickFormat) + : typeof tickFormat === "string" && scale.domain().length > 0 + ? (isTemporal(scale.domain()) ? utcFormat : format)(tickFormat) : tickFormat === undefined ? formatDefault - : typeof tickFormat === "string" - ? (isTemporal(scale.domain()) ? utcFormat : format)(tickFormat) : constant(tickFormat); } diff --git a/test/output/tickFormatEmptyDomain.svg b/test/output/tickFormatEmptyDomain.svg new file mode 100644 index 0000000000..38e50ca56f --- /dev/null +++ b/test/output/tickFormatEmptyDomain.svg @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/test/output/tickFormatEmptyFacetDomain.svg b/test/output/tickFormatEmptyFacetDomain.svg new file mode 100644 index 0000000000..38e50ca56f --- /dev/null +++ b/test/output/tickFormatEmptyFacetDomain.svg @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/test/plots/index.ts b/test/plots/index.ts index 3176991d75..f54398dca7 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -309,6 +309,7 @@ export * from "./style-overrides.js"; export * from "./symbol-set.js"; export * from "./text-overflow.js"; export * from "./this-is-just-to-say.js"; +export * from "./tick-format.js"; export * from "./time-axis.js"; export * from "./tip-format.js"; export * from "./tip.js"; diff --git a/test/plots/tick-format.ts b/test/plots/tick-format.ts new file mode 100644 index 0000000000..66b2898f9e --- /dev/null +++ b/test/plots/tick-format.ts @@ -0,0 +1,9 @@ +import * as Plot from "@observablehq/plot"; + +export async function tickFormatEmptyDomain() { + return Plot.plot({y: {tickFormat: "%W"}, marks: [Plot.barX([]), Plot.frame()]}); +} + +export async function tickFormatEmptyFacetDomain() { + return Plot.plot({fy: {tickFormat: "%W"}, marks: [Plot.barX([]), Plot.frame()]}); +} From 6bea18ef4f4c311aa524b3737f2b73cab6e30d50 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 19 Nov 2024 08:28:50 -0800 Subject: [PATCH 5/7] handle degenerate domains (#2212) --- src/scales.js | 9 +++--- src/scales/quantitative.js | 10 +++++-- test/output/colorLegendDomainEmpty.html | 27 ++++++++++++++++++ test/output/colorLegendDomainUnary.html | 29 ++++++++++++++++++++ test/output/colorLegendLinearDomainEmpty.svg | 17 ++++++++++++ test/output/colorLegendLinearDomainUnary.svg | 22 +++++++++++++++ test/plots/legend-color.ts | 16 +++++++++++ 7 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 test/output/colorLegendDomainEmpty.html create mode 100644 test/output/colorLegendDomainUnary.html create mode 100644 test/output/colorLegendLinearDomainEmpty.svg create mode 100644 test/output/colorLegendLinearDomainUnary.svg diff --git a/src/scales.js b/src/scales.js index 46a1f45c3c..7d01163ad0 100644 --- a/src/scales.js +++ b/src/scales.js @@ -425,10 +425,11 @@ function inferScaleType(key, channels, {type, domain, range, scheme, pivot, proj if (kind === opacity || kind === length) return "linear"; if (kind === symbol) return "ordinal"; - // If the domain or range has more than two values, assume it’s ordinal. You - // can still use a “piecewise” (or “polylinear”) scale, but you must set the - // type explicitly. - if ((domain || range || []).length > 2) return asOrdinalType(kind); + // If a domain or range is explicitly specified and doesn’t have two values, + // assume it’s ordinal. You can still use a “piecewise” (or “polylinear”) + // scale, but you must set the type explicitly. + const n = (domain ?? range)?.length; + if (n < 2 || n > 2) return asOrdinalType(kind); // Otherwise, infer the scale type from the data! Prefer the domain, if // present, over channels. (The domain and channels should be consistently diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 88feec4d07..8cf9cadbbb 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -80,6 +80,7 @@ export function createScaleQ( reverse } ) { + domain = maybeRepeat(domain); interval = maybeRangeInterval(interval, type); if (type === "cyclical" || type === "sequential") type = "linear"; // shorthand for color schemes if (typeof interpolate !== "function") interpolate = maybeInterpolator(interpolate); // named interpolator @@ -88,8 +89,8 @@ export function createScaleQ( // If an explicit range is specified, and it has a different length than the // domain, then redistribute the range using a piecewise interpolator. if (range !== undefined) { - const n = (domain = arrayify(domain)).length; - const m = (range = arrayify(range)).length; + const n = domain.length; + const m = (range = maybeRepeat(range)).length; if (n !== m) { if (interpolate.length === 1) throw new Error("invalid piecewise interpolator"); // e.g., turbo interpolate = piecewise(interpolate, range); @@ -137,6 +138,11 @@ export function createScaleQ( return {type, domain, range, scale, interpolate, interval}; } +function maybeRepeat(values) { + values = arrayify(values); + return values.length >= 2 ? values : [values[0], values[0]]; +} + function maybeNice(nice, type) { return nice === true ? undefined : typeof nice === "number" ? nice : maybeNiceInterval(nice, type); } diff --git a/test/output/colorLegendDomainEmpty.html b/test/output/colorLegendDomainEmpty.html new file mode 100644 index 0000000000..5e9bfac54d --- /dev/null +++ b/test/output/colorLegendDomainEmpty.html @@ -0,0 +1,27 @@ +
+ +
\ No newline at end of file diff --git a/test/output/colorLegendDomainUnary.html b/test/output/colorLegendDomainUnary.html new file mode 100644 index 0000000000..1401790db6 --- /dev/null +++ b/test/output/colorLegendDomainUnary.html @@ -0,0 +1,29 @@ +
+ + + 0 +
\ No newline at end of file diff --git a/test/output/colorLegendLinearDomainEmpty.svg b/test/output/colorLegendLinearDomainEmpty.svg new file mode 100644 index 0000000000..4863ce6e97 --- /dev/null +++ b/test/output/colorLegendLinearDomainEmpty.svg @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/test/output/colorLegendLinearDomainUnary.svg b/test/output/colorLegendLinearDomainUnary.svg new file mode 100644 index 0000000000..cf6b6c2f70 --- /dev/null +++ b/test/output/colorLegendLinearDomainUnary.svg @@ -0,0 +1,22 @@ + + + + + + + 0 + + + \ No newline at end of file diff --git a/test/plots/legend-color.ts b/test/plots/legend-color.ts index 744e850f4f..031dfdbc78 100644 --- a/test/plots/legend-color.ts +++ b/test/plots/legend-color.ts @@ -34,6 +34,22 @@ export function colorLegendCategoricalReverse() { return Plot.legend({color: {domain: "ABCDEFGHIJ", reverse: true}}); } +export function colorLegendDomainUnary() { + return Plot.legend({color: {domain: [0]}}); +} + +export function colorLegendDomainEmpty() { + return Plot.legend({color: {domain: []}}); +} + +export function colorLegendLinearDomainUnary() { + return Plot.legend({color: {type: "linear", domain: [0]}}); +} + +export function colorLegendLinearDomainEmpty() { + return Plot.legend({color: {type: "linear", domain: []}}); +} + export function colorLegendOrdinal() { return Plot.legend({color: {type: "ordinal", domain: "ABCDEFGHIJ"}}); } From eb854199b0496530975a93ab370a04c72dd86229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Nov 2024 21:48:41 +0100 Subject: [PATCH 6/7] waffle tips (#2132) * waffle tips * test faceting * simpler x * support waffle pointer, and simplify * crazy logic to compute the waffles' centroids * remove single hint * prettier * tidy * pass cell dimensions as channels * prettier * no filter on polygon * prettier * fix tip option type * fewer channels * default maxRadius to infinity --------- Co-authored-by: Mike Bostock --- src/mark.d.ts | 3 +- src/marks/waffle.js | 242 ++- test/output/wafflePointer.svg | 450 +++++ test/output/wafflePointerFractional.svg | 136 ++ test/output/waffleTip.svg | 67 + test/output/waffleTipFacet.svg | 2084 ++++++++++++++++++++++ test/output/waffleTipFacetX.svg | 2080 ++++++++++++++++++++++ test/output/waffleTipFacetXY.svg | 2085 +++++++++++++++++++++++ test/output/waffleTipUnit.svg | 447 +++++ test/output/waffleTipUnitX.svg | 447 +++++ test/output/waffleTipX.svg | 70 + test/plots/waffle.ts | 114 ++ 12 files changed, 8149 insertions(+), 76 deletions(-) create mode 100644 test/output/wafflePointer.svg create mode 100644 test/output/wafflePointerFractional.svg create mode 100644 test/output/waffleTip.svg create mode 100644 test/output/waffleTipFacet.svg create mode 100644 test/output/waffleTipFacetX.svg create mode 100644 test/output/waffleTipFacetXY.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/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: diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 4a95d38ec6..3b2b6d5774 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -1,9 +1,11 @@ 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"; import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js"; import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; @@ -14,8 +16,8 @@ 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, multiple, ...options} = {}) { + super(data, wafflePolygon("x", options), waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; this.round = maybeRound(round); @@ -24,8 +26,8 @@ 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, multiple, ...options} = {}) { + super(data, wafflePolygon("y", options), waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; this.round = maybeRound(round); @@ -33,17 +35,19 @@ export class WaffleY extends BarY { } } -function waffleRender(y) { - return function (index, scales, values, dimensions, context) { - const {ariaLabel, href, title, ...visualValues} = values; - const {unit, gap, rx, ry, round} = this; - const {document} = context; - const Y1 = values.channels[`${y}1`].value; - const Y2 = values.channels[`${y}2`].value; +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 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]); @@ -55,63 +59,98 @@ function waffleRender(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]; + // The reference position. const tx = (barwidth - multiple * cx) / 2; const x0 = typeof barx === "function" ? (i) => barx(i) + tx : barx + tx; const y0 = scales[y](0); - // 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${wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple) - .map(transform) - .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(); + // 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); + + 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(); // extract the transformed centroid + X[i] = c[ix] + mx(i); + Y[i] = c[iy] + y0; + } + + return { + channels: { + polygon: {value: P, source: null, filter: null}, + [`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]} + } + }; + }); +} + +function waffleRender({render, ...options}) { + return { + ...options, + render: composeRender(render, function (index, scales, values, dimensions, context) { + const {gap, rx, ry} = this; + const {channels, ariaLabel, href, title, ...visualValues} = values; + const {document} = context; + 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", 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", cx - gap); + basePatternRect.setAttribute("height", cy - 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", 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})`) + .call(applyChannelStyles, this, {ariaLabel, href, title}) + ) + .node(); + }) }; } -// 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: @@ -148,14 +187,11 @@ 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 - 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); @@ -177,9 +213,49 @@ function wafflePoints(i1, i2, columns) { points.push([x2f, y2c]); if (y2c > y1c) points.push([0, y2c]); } + points.push(waffleCentroid(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 + ? // 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 + [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 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 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) + ]; +} + function maybeRound(round) { if (round === undefined || round === false) return Number; if (round === true) return Math.round; @@ -200,12 +276,28 @@ 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: waffleTip(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: 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; } diff --git a/test/output/wafflePointer.svg b/test/output/wafflePointer.svg new file mode 100644 index 0000000000..d20a88f20c --- /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/output/wafflePointerFractional.svg b/test/output/wafflePointerFractional.svg new file mode 100644 index 0000000000..7c587be531 --- /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/output/waffleTip.svg b/test/output/waffleTip.svg new file mode 100644 index 0000000000..2d5277a659 --- /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/waffleTipFacet.svg b/test/output/waffleTipFacet.svg new file mode 100644 index 0000000000..4c35675277 --- /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..7b1ec373e7 --- /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..7923ce8524 --- /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/output/waffleTipUnit.svg b/test/output/waffleTipUnit.svg new file mode 100644 index 0000000000..41a47d10d2 --- /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..8fb0bb719b --- /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..72ca0297f9 --- /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 d5fe683bdf..8212911b19 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 @@ -247,6 +248,101 @@ 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 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 y1 = (values.channels.y1 as any).source.value; + const y2 = (values.channels.y2 as any).source.value; + return svg`${Array.from( + index, + (i) => + svg`${format(y2[i] - y1[i])}` + )}`; + } + }) + ] + }); +} + +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 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"}, + 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: {format: {x: false}}} + ) + ] + }); +} + export function waffleHref() { return Plot.plot({ inset: 10, @@ -265,6 +361,24 @@ export function waffleHref() { }); } +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}) + ] + }); +} + export function waffleShapes() { const k = 10; let offset = 0; From 5fb8616447a3e1cfd98841a458ac5f026fa0c34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Nov 2024 23:40:46 +0100 Subject: [PATCH 7/7] fix waffle stroke (when constant) (#2204) Co-authored-by: Mike Bostock --- src/marks/waffle.js | 2 +- test/output/waffleShapes.svg | 40 +-- test/output/waffleStroke.svg | 24 +- test/output/waffleStrokeMixed.svg | 12 +- test/output/waffleStrokeNegative.svg | 12 +- test/output/waffleStrokePositive.svg | 12 +- test/output/waffleStrokeWidth.svg | 346 +++++++++++++++++++++++++ test/output/waffleStrokeWidthConst.svg | 346 +++++++++++++++++++++++++ test/plots/waffle.ts | 14 + 9 files changed, 757 insertions(+), 51 deletions(-) create mode 100644 test/output/waffleStrokeWidth.svg create mode 100644 test/output/waffleStrokeWidthConst.svg diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 3b2b6d5774..1e848bcc7e 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -142,7 +142,7 @@ function waffleRender({render, ...options}) { .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})`) + .attr("stroke", this.stroke == null ? null : "none") .call(applyChannelStyles, this, {ariaLabel, href, title}) ) .node(); diff --git a/test/output/waffleShapes.svg b/test/output/waffleShapes.svg index c1e66aeb8a..798d51ad81 100644 --- a/test/output/waffleShapes.svg +++ b/test/output/waffleShapes.svg @@ -83,120 +83,120 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/test/output/waffleStroke.svg b/test/output/waffleStroke.svg index 2ade48d2f0..b8b4d65b17 100644 --- a/test/output/waffleStroke.svg +++ b/test/output/waffleStroke.svg @@ -66,12 +66,12 @@ - - - - - - + + + + + + @@ -92,11 +92,11 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/test/output/waffleStrokeMixed.svg b/test/output/waffleStrokeMixed.svg index fceae2f5af..a8fe5022d4 100644 --- a/test/output/waffleStrokeMixed.svg +++ b/test/output/waffleStrokeMixed.svg @@ -70,12 +70,12 @@ - - - - - - + + + + + + diff --git a/test/output/waffleStrokeNegative.svg b/test/output/waffleStrokeNegative.svg index 8e03565a10..263cc64772 100644 --- a/test/output/waffleStrokeNegative.svg +++ b/test/output/waffleStrokeNegative.svg @@ -100,12 +100,12 @@ - - - - - - + + + + + + diff --git a/test/output/waffleStrokePositive.svg b/test/output/waffleStrokePositive.svg index cb2ab9cd2b..7e6499571f 100644 --- a/test/output/waffleStrokePositive.svg +++ b/test/output/waffleStrokePositive.svg @@ -100,12 +100,12 @@ - - - - - - + + + + + + diff --git a/test/output/waffleStrokeWidth.svg b/test/output/waffleStrokeWidth.svg new file mode 100644 index 0000000000..1667ceb425 --- /dev/null +++ b/test/output/waffleStrokeWidth.svg @@ -0,0 +1,346 @@ + + + + + 0 + 10 + 20 + 30 + 40 + 50 + 60 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleStrokeWidthConst.svg b/test/output/waffleStrokeWidthConst.svg new file mode 100644 index 0000000000..8ca80f7ae4 --- /dev/null +++ b/test/output/waffleStrokeWidthConst.svg @@ -0,0 +1,346 @@ + + + + + 0 + 10 + 20 + 30 + 40 + 50 + 60 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts index 8212911b19..ead9a1bf96 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -361,6 +361,20 @@ export function waffleHref() { }); } +export function waffleStrokeWidth() { + return Plot.plot({ + inset: 10, + marks: [Plot.waffleY({length: 77}, {y: 1, stroke: (d, i) => i % 7, gap: 15, strokeWidth: 15, strokeOpacity: 0.8})] + }); +} + +export function waffleStrokeWidthConst() { + return Plot.plot({ + inset: 10, + marks: [Plot.waffleY({length: 77}, {y: 1, stroke: "black", gap: 15, strokeWidth: 15, strokeOpacity: 0.8})] + }); +} + export function waffleTipFacetX() { return Plot.plot({ height: 500,