-
Notifications
You must be signed in to change notification settings - Fork 197
waffle tips #2132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
waffle tips #2132
Changes from 8 commits
7dcccea
da7d094
1f0d634
caa5009
83d0a4d
f27e321
c7ef726
0233683
8ef4c82
57f8e3c
0c7224f
e927e69
4ef5034
1f4518c
28bdcaf
bed5d25
a67bd8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 {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"; | ||
|
|
@@ -15,7 +17,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); | ||
| options = initializer({...options, render: composeRender(render, waffleRender("x"))}, waffleInitializer("x")); | ||
| super(data, options, waffleDefaults); | ||
| this.unit = Math.max(0, unit); | ||
| this.gap = +gap; | ||
| this.round = maybeRound(round); | ||
|
|
@@ -25,19 +28,20 @@ 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); | ||
| options = initializer({...options, render: composeRender(render, waffleRender("y"))}, waffleInitializer("y")); | ||
| super(data, options, waffleDefaults); | ||
| this.unit = Math.max(0, unit); | ||
| this.gap = +gap; | ||
| this.round = maybeRound(round); | ||
| this.multiple = maybeMultiple(multiple); | ||
| } | ||
| } | ||
|
|
||
| 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; | ||
| 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; | ||
|
|
||
|
|
@@ -55,12 +59,51 @@ 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); | ||
|
|
||
| // 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(); | ||
| X[i] = c[ix] + mx(i); | ||
| Y[i] = c[iy] + y0; | ||
| } | ||
|
|
||
| this.cx = cx; | ||
| this.cy = cy; | ||
| this.x0 = x0; | ||
| this.y0 = y0; | ||
mbostock marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return { | ||
| channels: { | ||
| polygon: {value: P, source: 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`]} | ||
| } | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| 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; | ||
|
|
||
| // Create a base pattern with shared attributes for cloning. | ||
| const patternId = getPatternId(); | ||
| const basePattern = document.createElementNS(namespaces.svg, "pattern"); | ||
|
|
@@ -96,13 +139,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})`) | ||
| .call(applyChannelStyles, this, {ariaLabel, href, title}) | ||
|
|
@@ -148,6 +185,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 | ||
|
|
@@ -177,9 +216,42 @@ function wafflePoints(i1, i2, columns) { | |
| points.push([x2f, y2c]); | ||
| if (y2c > y1c) points.push([0, y2c]); | ||
| } | ||
| points.push(centroid(i1, i2, columns)); | ||
| return points; | ||
| } | ||
|
|
||
| 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 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; | ||
|
|
@@ -200,12 +272,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)))}); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.