From 8b1e22312e36a0bb4ac5b491300a75942a8c193d Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 28 Mar 2021 11:39:43 -0700 Subject: [PATCH 1/8] precompute scaled channels --- src/marks/bar.js | 29 ++++++++++++++--------------- src/plot.js | 10 +++++++--- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/marks/bar.js b/src/marks/bar.js index 57ff15a27c..fa5385b1cc 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -46,7 +46,6 @@ export class AbstractBar extends Mark { } render(I, scales, channels, dimensions) { const {rx, ry} = this; - const {color} = scales; const {z: Z, title: L, fill: F, stroke: S} = channels; const index = filter(I, ...this._positions(channels), F, S); if (Z) index.sort((i, j) => ascending(Z[i], Z[j])); @@ -61,20 +60,20 @@ export class AbstractBar extends Mark { .attr("width", this._width(scales, channels, dimensions)) .attr("y", this._y(scales, channels, dimensions)) .attr("height", this._height(scales, channels, dimensions)) - .attr("fill", F && (i => color(F[i]))) - .attr("stroke", S && (i => color(S[i]))) + .attr("fill", F && (i => F[i])) + .attr("stroke", S && (i => S[i])) .call(rx != null ? rect => rect.attr("rx", rx) : () => {}) .call(ry != null ? rect => rect.attr("ry", ry) : () => {}) .call(title(L))) .node(); } - _x({x}, {x: X}, {marginLeft}) { + _x(scales, {x: X}, {marginLeft}) { const {insetLeft} = this; - return X ? i => x(X[i]) + insetLeft : marginLeft + insetLeft; + return X ? i => X[i] + insetLeft : marginLeft + insetLeft; } - _y({y}, {y: Y}, {marginTop}) { + _y(scales, {y: Y}, {marginTop}) { const {insetTop} = this; - return Y ? i => y(Y[i]) + insetTop : marginTop + insetTop; + return Y ? i => Y[i] + insetTop : marginTop + insetTop; } _width({x}, {x: X}, {marginRight, marginLeft, width}) { const {insetLeft, insetRight} = this; @@ -106,13 +105,13 @@ export class BarX extends AbstractBar { _positions({x1: X1, x2: X2, y: Y}) { return [X1, X2, Y]; } - _x({x}, {x1: X1, x2: X2}) { + _x(scales, {x1: X1, x2: X2}) { const {insetLeft} = this; - return i => Math.min(x(X1[i]), x(X2[i])) + insetLeft; + return i => Math.min(X1[i], X2[i]) + insetLeft; } - _width({x}, {x1: X1, x2: X2}) { + _width(scales, {x1: X1, x2: X2}) { const {insetLeft, insetRight} = this; - return i => Math.max(0, Math.abs(x(X2[i]) - x(X1[i])) - insetLeft - insetRight); + return i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight); } } @@ -134,13 +133,13 @@ export class BarY extends AbstractBar { _positions({y1: Y1, y2: Y2, x: X}) { return [Y1, Y2, X]; } - _y({y}, {y1: Y1, y2: Y2}) { + _y(scales, {y1: Y1, y2: Y2}) { const {insetTop} = this; - return i => Math.min(y(Y1[i]), y(Y2[i])) + insetTop; + return i => Math.min(Y1[i], Y2[i]) + insetTop; } - _height({y}, {y1: Y1, y2: Y2}) { + _height(scales, {y1: Y1, y2: Y2}) { const {insetTop, insetBottom} = this; - return i => Math.max(0, Math.abs(y(Y2[i]) - y(Y1[i])) - insetTop - insetBottom); + return i => Math.max(0, Math.abs(Y2[i] - Y1[i]) - insetTop - insetBottom); } } diff --git a/src/plot.js b/src/plot.js index e32214a123..c7f0cba7e5 100644 --- a/src/plot.js +++ b/src/plot.js @@ -38,9 +38,13 @@ export function plot(options = {}) { if (scaled) scaled.push(channel); else scaleChannels.set(scale, [channel]); } - if (name !== undefined) { - named[name] = channel.value; - } + if (name !== undefined) Object.defineProperty(named, name, { + get: () => { + const value = scale === undefined ? channel.value : Array.from(channel.value, scales[scale]); // TODO type + console.log({name, value}); + return value; + } + }); } markChannels.set(mark, named); markIndex.set(mark, index); From 9ae31c0298e61c68e8d4e3d80f859f89c1a96dd1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 28 Mar 2021 11:44:28 -0700 Subject: [PATCH 2/8] comments --- src/plot.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/plot.js b/src/plot.js index c7f0cba7e5..d5e02f28c0 100644 --- a/src/plot.js +++ b/src/plot.js @@ -38,13 +38,14 @@ export function plot(options = {}) { if (scaled) scaled.push(channel); else scaleChannels.set(scale, [channel]); } - if (name !== undefined) Object.defineProperty(named, name, { - get: () => { - const value = scale === undefined ? channel.value : Array.from(channel.value, scales[scale]); // TODO type - console.log({name, value}); - return value; - } - }); + // TODO use Float64Array.from for position and radius scales? + // TODO don’t use a getter, and instead re-assign the channel.value after construcing scales + // TODO test that this works with faceting + if (name !== undefined) { + Object.defineProperty(named, name, { + get: () => scale === undefined ? channel.value : Array.from(channel.value, scales[scale]) + }); + } } markChannels.set(mark, named); markIndex.set(mark, index); From a936dd5b20cf96cdf2a422c143afa79e5c3270d8 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 28 Mar 2021 16:30:23 -0700 Subject: [PATCH 3/8] cleaner, rule --- src/facet.js | 13 ++++++------- src/mark.js | 11 +++++++++++ src/marks/rule.js | 24 ++++++++++++------------ src/plot.js | 16 ++++------------ 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/facet.js b/src/facet.js index 52f60163f7..26d311de79 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,6 +1,6 @@ import {cross, groups, InternMap} from "d3"; import {create} from "d3"; -import {Mark, first, second} from "./mark.js"; +import {Mark, values, first, second} from "./mark.js"; export function facets(data, {x, y, ...options}, marks) { return x === undefined && y === undefined @@ -58,12 +58,10 @@ class Facet extends Mark { marksIndex[i] = index; } } - const named = Object.create(null); - for (const [name, channel] of channels) { - if (name !== undefined) Object.defineProperty(named, name, {get: () => channel.value}); // scale transform - subchannels.push([undefined, channel]); + for (const [, channel] of channels) { + subchannels.push([, channel]); } - marksChannels.push(named); + marksChannels.push(channels); } return {index, channels: [...channels, ...subchannels]}; } @@ -73,6 +71,7 @@ class Facet extends Mark { const fyMargins = fy && {marginTop: 0, marginBottom: 0, height: fy.bandwidth()}; const fxMargins = fx && {marginRight: 0, marginLeft: 0, width: fx.bandwidth()}; const subdimensions = {...dimensions, ...fxMargins, ...fyMargins}; + const marksValues = marksChannels.map(channels => values(channels, scales)); return create("svg:g") .call(g => { if (fy && axes.y) { @@ -109,7 +108,7 @@ class Facet extends Mark { const node = marks[i].render( marksFacetIndex[i], scales, - marksChannels[i], + marksValues[i], subdimensions ); if (node != null) this.appendChild(node); diff --git a/src/mark.js b/src/mark.js index 8791c6ccae..630611c297 100644 --- a/src/mark.js +++ b/src/mark.js @@ -276,3 +276,14 @@ export function numberChannel(source) { label: labelof(source) }; } + +// TODO use Float64Array.from for position and radius scales? +export function values(channels = [], scales) { + const values = Object.create(null); + for (const [name, {value, scale}] of channels) { + if (name !== undefined) { + values[name] = scale === undefined ? value : Array.from(value, scales[scale]); + } + } + return values; +} diff --git a/src/marks/rule.js b/src/marks/rule.js index 87478c5e55..e49fe87d31 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -34,7 +34,7 @@ export class RuleX extends Mark { } render( I, - {x, y, color}, + {x, y}, {x: X, y1: Y1, y2: Y2, z: Z, title: L, stroke: S}, {width, height, marginTop, marginRight, marginLeft, marginBottom} ) { @@ -47,11 +47,11 @@ export class RuleX extends Mark { .data(index) .join("line") .call(applyDirectStyles, this) - .attr("x1", X ? i => Math.round(x(X[i])) : (marginLeft + width - marginRight) / 2) - .attr("x2", X ? i => Math.round(x(X[i])) : (marginLeft + width - marginRight) / 2) - .attr("y1", Y1 ? i => y(Y1[i]) : marginTop) - .attr("y2", Y2 ? (y.bandwidth ? i => y(Y2[i]) + y.bandwidth() : i => y(Y2[i])) : height - marginBottom) - .attr("stroke", S && (i => color(S[i]))) + .attr("x1", X ? i => Math.round(X[i]) : (marginLeft + width - marginRight) / 2) + .attr("x2", X ? i => Math.round(X[i]) : (marginLeft + width - marginRight) / 2) + .attr("y1", Y1 ? i => Y1[i] : marginTop) + .attr("y2", Y2 ? (y.bandwidth ? i => Y2[i] + y.bandwidth() : i => Y2[i]) : height - marginBottom) + .attr("stroke", S && (i => S[i])) .call(title(L))) .node(); } @@ -87,7 +87,7 @@ export class RuleY extends Mark { } render( I, - {x, y, color}, + {x, y}, {y: Y, x1: X1, x2: X2, z: Z, title: L, stroke: S}, {width, height, marginTop, marginRight, marginLeft, marginBottom} ) { @@ -100,11 +100,11 @@ export class RuleY extends Mark { .data(index) .join("line") .call(applyDirectStyles, this) - .attr("x1", X1 ? i => x(X1[i]) : marginLeft) - .attr("x2", X2 ? (x.bandwidth ? i => x(X2[i]) + x.bandwidth() : i => x(X2[i])) : width - marginRight) - .attr("y1", Y ? i => Math.round(y(Y[i])) : (marginTop + height - marginBottom) / 2) - .attr("y2", Y ? i => Math.round(y(Y[i])) : (marginTop + height - marginBottom) / 2) - .attr("stroke", S && (i => color(S[i]))) + .attr("x1", X1 ? i => X1[i] : marginLeft) + .attr("x2", X2 ? (x.bandwidth ? i => X2[i] + x.bandwidth() : i => X2[i]) : width - marginRight) + .attr("y1", Y ? i => Math.round(Y[i]) : (marginTop + height - marginBottom) / 2) + .attr("y2", Y ? i => Math.round(Y[i]) : (marginTop + height - marginBottom) / 2) + .attr("stroke", S && (i => S[i])) .call(title(L))) .node(); } diff --git a/src/plot.js b/src/plot.js index d5e02f28c0..2665465c66 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,6 +1,7 @@ import {create} from "d3"; import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js"; import {facets} from "./facet.js"; +import {values} from "./mark.js"; import {Scales, autoScaleRange} from "./scales.js"; export function plot(options = {}) { @@ -27,9 +28,8 @@ export function plot(options = {}) { // Also apply any scale transforms. for (const mark of marks) { if (markChannels.has(mark)) throw new Error("duplicate mark"); - const named = Object.create(null); const {index, channels} = mark.initialize(); - for (const [name, channel] of channels) { + for (const [, channel] of channels) { const {scale} = channel; if (scale !== undefined) { const scaled = scaleChannels.get(scale); @@ -38,16 +38,8 @@ export function plot(options = {}) { if (scaled) scaled.push(channel); else scaleChannels.set(scale, [channel]); } - // TODO use Float64Array.from for position and radius scales? - // TODO don’t use a getter, and instead re-assign the channel.value after construcing scales - // TODO test that this works with faceting - if (name !== undefined) { - Object.defineProperty(named, name, { - get: () => scale === undefined ? channel.value : Array.from(channel.value, scales[scale]) - }); - } } - markChannels.set(mark, named); + markChannels.set(mark, channels); markIndex.set(mark, index); } @@ -90,7 +82,7 @@ export function plot(options = {}) { for (const mark of marks) { const channels = markChannels.get(mark); const index = markIndex.get(mark); - const node = mark.render(index, scales, channels, dimensions, axes); + const node = mark.render(index, scales, values(channels, scales), dimensions, axes); if (node != null) svg.append(() => node); } From 88d62ad8419576d22ce96d1f586730ad64cbeca7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 28 Mar 2021 16:36:31 -0700 Subject: [PATCH 4/8] fully precomputed --- src/marks/area.js | 14 +++++++------- src/marks/dot.js | 12 ++++++------ src/marks/line.js | 10 +++++----- src/marks/link.js | 12 ++++++------ src/marks/rect.js | 14 +++++++------- src/marks/text.js | 18 +++++++++--------- src/marks/tick.js | 31 +++++++++++++++---------------- 7 files changed, 55 insertions(+), 56 deletions(-) diff --git a/src/marks/area.js b/src/marks/area.js index c9fc91ae65..3e333d4b3a 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -49,7 +49,7 @@ export class Area extends Mark { ...options }); } - render(I, {x, y, color}, {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z, title: L, fill: F, stroke: S}) { + render(I, {x, y}, {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z, title: L, fill: F, stroke: S}) { return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, x, y) @@ -57,15 +57,15 @@ export class Area extends Mark { .data(Z ? group(I, i => Z[i]).values() : [I]) .join("path") .call(applyDirectStyles, this) - .attr("fill", F && (([i]) => color(F[i]))) - .attr("stroke", S && (([i]) => color(S[i]))) + .attr("fill", F && (([i]) => F[i])) + .attr("stroke", S && (([i]) => S[i])) .attr("d", shapeArea() .curve(this.curve) .defined(i => defined(X1[i]) && defined(Y1[i]) && defined(X2[i]) && defined(Y2[i])) - .x0(i => x(X1[i])) - .y0(i => y(Y1[i])) - .x1(i => x(X2[i])) - .y1(i => y(Y2[i]))) + .x0(i => X1[i]) + .y0(i => Y1[i]) + .x1(i => X2[i]) + .y1(i => Y2[i])) .call(titleGroup(L))) .node(); } diff --git a/src/marks/dot.js b/src/marks/dot.js index 59017c6147..eb6b1eb1d3 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -44,7 +44,7 @@ export class Dot extends Mark { } render( I, - {x, y, r, color}, + {x, y}, {x: X, y: Y, z: Z, r: R, title: L, fill: F, stroke: S}, {width, height, marginTop, marginRight, marginBottom, marginLeft} ) { @@ -58,11 +58,11 @@ export class Dot extends Mark { .data(index) .join("circle") .call(applyDirectStyles, this) - .attr("cx", X ? i => x(X[i]) : (marginLeft + width - marginRight) / 2) - .attr("cy", Y ? i => y(Y[i]) : (marginTop + height - marginBottom) / 2) - .attr("r", R ? i => r(R[i]) : this.r) - .attr("fill", F && (i => color(F[i]))) - .attr("stroke", S && (i => color(S[i]))) + .attr("cx", X ? i => X[i] : (marginLeft + width - marginRight) / 2) + .attr("cy", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2) + .attr("r", R ? i => R[i] : this.r) + .attr("fill", F && (i => F[i])) + .attr("stroke", S && (i => S[i])) .call(title(L))) .node(); } diff --git a/src/marks/line.js b/src/marks/line.js index 50c689a449..e93fa97ab6 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -46,7 +46,7 @@ export class Line extends Mark { ...options }); } - render(I, {x, y, color}, {x: X, y: Y, z: Z, title: L, fill: F, stroke: S}) { + render(I, {x, y}, {x: X, y: Y, z: Z, title: L, fill: F, stroke: S}) { return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, x, y, 0.5, 0.5) @@ -54,13 +54,13 @@ export class Line extends Mark { .data(Z ? group(I, i => Z[i]).values() : [I]) .join("path") .call(applyDirectStyles, this) - .attr("fill", F && (([i]) => color(F[i]))) - .attr("stroke", S && (([i]) => color(S[i]))) + .attr("fill", F && (([i]) => F[i])) + .attr("stroke", S && (([i]) => S[i])) .attr("d", shapeLine() .curve(this.curve) .defined(i => defined(X[i]) && defined(Y[i])) - .x(i => x(X[i])) - .y(i => y(Y[i]))) + .x(i => X[i]) + .y(i => Y[i])) .call(titleGroup(L))) .node(); } diff --git a/src/marks/link.js b/src/marks/link.js index 50044804ad..a45d145744 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -36,7 +36,7 @@ export class Link extends Mark { } render( I, - {x, y, color}, + {x, y}, {x1: X1, y1: Y1, x2: X2, y2: Y2, z: Z, title: L, stroke: S} ) { const index = filter(I, X1, Y1, X2, Y2, S); @@ -48,11 +48,11 @@ export class Link extends Mark { .data(index) .join("line") .call(applyDirectStyles, this) - .attr("x1", i => x(X1[i])) - .attr("y1", i => y(Y1[i])) - .attr("x2", i => x(X2[i])) - .attr("y2", i => y(Y2[i])) - .attr("stroke", S && (i => color(S[i]))) + .attr("x1", i => X1[i]) + .attr("y1", i => Y1[i]) + .attr("x2", i => X2[i]) + .attr("y2", i => Y2[i]) + .attr("stroke", S && (i => S[i])) .call(title(L))) .node(); } diff --git a/src/marks/rect.js b/src/marks/rect.js index a2f701541a..7944d967a4 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -52,7 +52,7 @@ export class Rect extends Mark { } render( I, - {x, y, color}, + {x, y}, {x1: X1, y1: Y1, x2: X2, y2: Y2, z: Z, title: L, fill: F, stroke: S} ) { const {rx, ry} = this; @@ -65,14 +65,14 @@ export class Rect extends Mark { .data(index) .join("rect") .call(applyDirectStyles, this) - .attr("x", i => Math.min(x(X1[i]), x(X2[i])) + this.insetLeft) - .attr("y", i => Math.min(y(Y1[i]), y(Y2[i])) + this.insetTop) - .attr("width", i => Math.max(0, Math.abs(x(X2[i]) - x(X1[i])) - this.insetLeft - this.insetRight)) - .attr("height", i => Math.max(0, Math.abs(y(Y1[i]) - y(Y2[i])) - this.insetTop - this.insetBottom)) + .attr("x", i => Math.min(X1[i], X2[i]) + this.insetLeft) + .attr("y", i => Math.min(Y1[i], Y2[i]) + this.insetTop) + .attr("width", i => Math.max(0, Math.abs(X2[i] - X1[i]) - this.insetLeft - this.insetRight)) + .attr("height", i => Math.max(0, Math.abs(Y1[i] - Y2[i]) - this.insetTop - this.insetBottom)) .call(rx != null ? rect => rect.attr("rx", rx) : () => {}) .call(ry != null ? rect => rect.attr("ry", ry) : () => {}) - .attr("fill", F && (i => color(F[i]))) - .attr("stroke", S && (i => color(S[i]))) + .attr("fill", F && (i => F[i])) + .attr("stroke", S && (i => S[i])) .call(title(L))) .node(); } diff --git a/src/marks/text.js b/src/marks/text.js index 203e6b437d..1eccc10c14 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -54,7 +54,7 @@ export class Text extends Mark { } render( I, - {x, y, color}, + {x, y}, {x: X, y: Y, z: Z, rotate: R, text: T, title: L, fill: F}, {width, height, marginTop, marginRight, marginBottom, marginLeft} ) { @@ -70,16 +70,16 @@ export class Text extends Mark { .data(index) .join("text") .call(applyDirectTextStyles, this) - .call(R ? text => text.attr("transform", X && Y ? i => `translate(${x(X[i])},${y(Y[i])}) rotate(${R[i]})` - : X ? i => `translate(${x(X[i])},${cy}) rotate(${R[i]})` - : Y ? i => `translate(${cx},${y(Y[i])}) rotate(${R[i]})` + .call(R ? text => text.attr("transform", X && Y ? i => `translate(${X[i]},${Y[i]}) rotate(${R[i]})` + : X ? i => `translate(${X[i]},${cy}) rotate(${R[i]})` + : Y ? i => `translate(${cx},${Y[i]}) rotate(${R[i]})` : i => `translate(${cx},${cy}) rotate(${R[i]})`) - : rotate ? text => text.attr("transform", X && Y ? i => `translate(${x(X[i])},${y(Y[i])}) rotate(${rotate})` - : X ? i => `translate(${x(X[i])},${cy}) rotate(${rotate})` - : Y ? i => `translate(${cx},${y(Y[i])}) rotate(${rotate})` + : rotate ? text => text.attr("transform", X && Y ? i => `translate(${X[i]},${Y[i]}) rotate(${rotate})` + : X ? i => `translate(${X[i]},${cy}) rotate(${rotate})` + : Y ? i => `translate(${cx},${Y[i]}) rotate(${rotate})` : `translate(${cx},${cy}) rotate(${rotate})`) - : text => text.attr("x", X ? i => x(X[i]) : cx).attr("y", Y ? i => y(Y[i]) : cy)) - .attr("fill", F && (i => color(F[i]))) + : text => text.attr("x", X ? i => X[i] : cx).attr("y", Y ? i => Y[i] : cy)) + .attr("fill", F && (i => F[i])) .text(i => T[i]) .call(title(L))) .node(); diff --git a/src/marks/tick.js b/src/marks/tick.js index b349ac3f5a..7f0e2cad46 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -29,7 +29,6 @@ class AbstractTick extends Mark { Style(this, {stroke: cstroke, ...options}); } render(I, scales, channels, dimensions) { - const {color} = scales; const {x: X, y: Y, z: Z, title: L, stroke: S} = channels; const index = filter(I, X, Y, S); if (Z) index.sort((i, j) => ascending(Z[i], Z[j])); @@ -44,7 +43,7 @@ class AbstractTick extends Mark { .attr("x2", this._x2(scales, channels, dimensions)) .attr("y1", this._y1(scales, channels, dimensions)) .attr("y2", this._y2(scales, channels, dimensions)) - .attr("stroke", S && (i => color(S[i]))) + .attr("stroke", S && (i => S[i])) .call(title(L))) .node(); } @@ -64,17 +63,17 @@ export class TickX extends AbstractTick { _transform(selection, {x}) { selection.call(applyTransform, x, null, 0.5, 0); } - _x1({x}, {x: X}) { - return i => Math.round(x(X[i])); + _x1(scales, {x: X}) { + return i => Math.round(X[i]); } - _x2({x}, {x: X}) { - return i => Math.round(x(X[i])); + _x2(scales, {x: X}) { + return i => Math.round(X[i]); } - _y1({y}, {y: Y}, {marginTop}) { - return Y ? i => y(Y[i]) : marginTop; + _y1(scales, {y: Y}, {marginTop}) { + return Y ? i => Y[i] : marginTop; } _y2({y}, {y: Y}, {height, marginBottom}) { - return Y ? i => y(Y[i]) + y.bandwidth() : height - marginBottom; + return Y ? i => Y[i] + y.bandwidth() : height - marginBottom; } } @@ -92,17 +91,17 @@ export class TickY extends AbstractTick { _transform(selection, {y}) { selection.call(applyTransform, null, y, 0, 0.5); } - _x1({x}, {x: X}, {marginLeft}) { - return X ? i => x(X[i]) : marginLeft; + _x1(scales, {x: X}, {marginLeft}) { + return X ? i => X[i] : marginLeft; } _x2({x}, {x: X}, {width, marginRight}) { - return X ? i => x(X[i]) + x.bandwidth() : width - marginRight; + return X ? i => X[i] + x.bandwidth() : width - marginRight; } - _y1({y}, {y: Y}) { - return i => Math.round(y(Y[i])); + _y1(scales, {y: Y}) { + return i => Math.round(Y[i]); } - _y2({y}, {y: Y}) { - return i => Math.round(y(Y[i])); + _y2(scales, {y: Y}) { + return i => Math.round(Y[i]); } } From e05054a5732a01074f678383dfb557de456e8a30 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 28 Mar 2021 16:44:55 -0700 Subject: [PATCH 5/8] nullsafe scale --- src/plot.js | 9 ++++++++- test/output/logDegenerate.svg | 1 - test/output/penguinSex.svg | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plot.js b/src/plot.js index 2665465c66..e65684856d 100644 --- a/src/plot.js +++ b/src/plot.js @@ -127,5 +127,12 @@ function Dimensions( } function ScaleFunctions(scales) { - return Object.fromEntries(Object.entries(scales).map(([name, {scale}]) => [name, scale])); + return Object.fromEntries(Object.entries(scales).map(([name, scale]) => [name, nullsafe(scale)])); +} + +// TODO fix d3-scale to treat null as undefined instead of coercing to zero +function nullsafe({type, scale}) { + return type === "quantitative" + ? Object.assign(x => x === null ? NaN : scale(x), scale) + : scale; } diff --git a/test/output/logDegenerate.svg b/test/output/logDegenerate.svg index 8f170ebf58..75859ac52e 100644 --- a/test/output/logDegenerate.svg +++ b/test/output/logDegenerate.svg @@ -59,7 +59,6 @@ - diff --git a/test/output/penguinSex.svg b/test/output/penguinSex.svg index 11d4703788..89c527bc73 100644 --- a/test/output/penguinSex.svg +++ b/test/output/penguinSex.svg @@ -42,6 +42,7 @@ + From fce25623928b0d0a9fe2abac331d84de3684a5c6 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 28 Mar 2021 16:49:54 -0700 Subject: [PATCH 6/8] comment --- src/plot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot.js b/src/plot.js index e65684856d..f3ae69bfd9 100644 --- a/src/plot.js +++ b/src/plot.js @@ -130,7 +130,7 @@ function ScaleFunctions(scales) { return Object.fromEntries(Object.entries(scales).map(([name, scale]) => [name, nullsafe(scale)])); } -// TODO fix d3-scale to treat null as undefined instead of coercing to zero +// TODO https://github.com/d3/d3-scale/pull/241/files function nullsafe({type, scale}) { return type === "quantitative" ? Object.assign(x => x === null ? NaN : scale(x), scale) From 5efee00ac86ace55d25156aadc1723d25615530c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 29 Mar 2021 16:06:40 +0200 Subject: [PATCH 7/8] penguin-species-island-sex example plot (#281) --- test/output/penguinSpeciesIslandSex.svg | 122 +++++++++++++++++++++++ test/plots/index.js | 1 + test/plots/penguin-species-island-sex.js | 29 ++++++ 3 files changed, 152 insertions(+) create mode 100644 test/output/penguinSpeciesIslandSex.svg create mode 100644 test/plots/penguin-species-island-sex.js diff --git a/test/output/penguinSpeciesIslandSex.svg b/test/output/penguinSpeciesIslandSex.svg new file mode 100644 index 0000000000..90995efe37 --- /dev/null +++ b/test/output/penguinSpeciesIslandSex.svg @@ -0,0 +1,122 @@ + + + + 0 + + + + 10 + + + + 20 + + + + 30 + + + + 40 + + + + 50 + + + + 60 + + + + 70 + + ↑ Frequency + + + + Adelie + + + Gentoo + + + Chinstrap + species + + + + + + MALE + + + FEMALE + + + N/A + + + + + + + MALE + + + FEMALE + + + N/A + sex + + + + + + MALE + + + FEMALE + + + N/A + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index 41dcfcbc5f..4c4ef81402 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -63,6 +63,7 @@ export {default as penguinSexMassCulmenSpecies} from "./penguin-sex-mass-culmen- export {default as penguinSpeciesGroup} from "./penguin-species-group.js"; export {default as penguinSpeciesIsland} from "./penguin-species-island.js"; export {default as penguinSpeciesIslandRelative} from "./penguin-species-island-relative.js"; +export {default as penguinSpeciesIslandSex} from "./penguin-species-island-sex.js"; export {default as policeDeaths} from "./police-deaths.js"; export {default as policeDeathsBar} from "./police-deaths-bar.js"; export {default as randomWalk} from "./random-walk.js"; diff --git a/test/plots/penguin-species-island-sex.js b/test/plots/penguin-species-island-sex.js new file mode 100644 index 0000000000..a22c9f1471 --- /dev/null +++ b/test/plots/penguin-species-island-sex.js @@ -0,0 +1,29 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const data = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + facet: { + data, + x: "species" + }, + fx: { + domain: d3.groupSort(data, ({length}) => length, d => d.species).reverse() + }, + x: { + domain: ["MALE", "FEMALE", null], + tickFormat: d => d === null ? "N/A" : d + }, + y: { + grid: true + }, + color: { + scheme: "greys" + }, + marks: [ + Plot.barY(data, Plot.stackY(Plot.groupX({y: "count"}, {x: "sex", fill: "island", stroke: "black"}))), + Plot.ruleY([0]) + ] + }); +} From 4e1bfd8b6d9cdd93049faa7fbf00312f19f69b94 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 29 Mar 2021 09:43:31 -0700 Subject: [PATCH 8/8] update dependencies --- package.json | 2 +- src/plot.js | 9 +------ yarn.lock | 74 +++++++++++++++++++++++++++++++++------------------- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 9a836a6d03..d5ce41521d 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "tape-await": "^0.1.2" }, "dependencies": { - "d3": "^6.6.0" + "d3": "^6.6.2" }, "publishConfig": { "registry": "https://npm.pkg.github.com" diff --git a/src/plot.js b/src/plot.js index f3ae69bfd9..2665465c66 100644 --- a/src/plot.js +++ b/src/plot.js @@ -127,12 +127,5 @@ function Dimensions( } function ScaleFunctions(scales) { - return Object.fromEntries(Object.entries(scales).map(([name, scale]) => [name, nullsafe(scale)])); -} - -// TODO https://github.com/d3/d3-scale/pull/241/files -function nullsafe({type, scale}) { - return type === "quantitative" - ? Object.assign(x => x === null ? NaN : scale(x), scale) - : scale; + return Object.fromEntries(Object.entries(scales).map(([name, {scale}]) => [name, scale])); } diff --git a/yarn.lock b/yarn.lock index 9c6c0c8df8..eca9c91f5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -104,7 +104,7 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.5: +acorn@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== @@ -119,10 +119,10 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^7.0.2: - version "7.2.4" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.4.tgz#8e239d4d56cf884bccca8cca362f508446dc160f" - integrity sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A== +ajv@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.0.1.tgz#dac101898a87f8ebb57fea69617e8096523c628c" + integrity sha512-46ZA4TalFcLLqX1dEU3dhdY38wAtDydJ4e7QQTVekLUTzXkb1LfqU6VOBXC/a9wiv4T094WURqJH6ZitF92Kqw== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -527,9 +527,9 @@ d3-scale-chromatic@2: d3-interpolate "1 - 2" d3-scale@3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.3.tgz#be380f57f1f61d4ff2e6cbb65a40593a51649cfd" - integrity sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g== + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.4.tgz#13d758d7cf4e4f1fc40196a63597d01f3ed81765" + integrity sha512-PG6gtpbPCFqKbvdBEswQcJcTzHC8VEd/XzezF5e68KlkT4/ggELw/nR1tv863jY6ufKTvDlzCMZvhe06codbbA== dependencies: d3-array "^2.3.0" d3-format "1 - 2" @@ -588,10 +588,10 @@ d3-zoom@2: d3-selection "2" d3-transition "2" -d3@^6.6.0: - version "6.6.1" - resolved "https://registry.yarnpkg.com/d3/-/d3-6.6.1.tgz#b3c381cae3f8ad7d8ce77f087ce0430f73ea9824" - integrity sha512-JzCj5GtMYoGCTD+8k1VwKzxTz1KScwHLQHuop8dne5uOc+WtqWEe2smqQrlMyfiCy9ohaD33LFGNTO5Xq3Mtiw== +d3@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/d3/-/d3-6.6.2.tgz#5d2133298b7adbf065d8274b77b36b429f6da3ee" + integrity sha512-wvC9cZe05bUCo00VFKXLQJWmWhGv0U43Qv0gn+tkl144S7bV22E80Gnp06BEuJVuwVfa6+S8UOfl8H9Ru/cmgA== dependencies: d3-array "2" d3-axis "2" @@ -1347,12 +1347,12 @@ jsbn@~0.1.0: integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= jsdom@^16.4.0: - version "16.5.1" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.1.tgz#4ced6bbd7b77d67fb980e64d9e3e6fb900f97dd6" - integrity sha512-pF73EOsJgwZekbDHEY5VO/yKXUkab/DuvrQB/ANVizbr6UAHJsDdHXuotZYwkJSGQl1JM+ivXaqY+XBDDL4TiA== + version "16.5.2" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.2.tgz#583fac89a0aea31dbf6237e7e4bedccd9beab472" + integrity sha512-JxNtPt9C1ut85boCbJmffaQ06NBnzkQY/MWO3YxPW8IWS38A26z+B1oBvA9LwKrytewdfymnhi4UNH3/RAgZrg== dependencies: abab "^2.0.5" - acorn "^8.0.5" + acorn "^8.1.0" acorn-globals "^6.0.0" cssom "^0.4.4" cssstyle "^2.3.0" @@ -1374,7 +1374,7 @@ jsdom@^16.4.0: webidl-conversions "^6.1.0" whatwg-encoding "^1.0.5" whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" + whatwg-url "^8.5.0" ws "^7.4.4" xml-name-validator "^3.0.0" @@ -1434,7 +1434,22 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + +lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1814,9 +1829,9 @@ rollup-plugin-terser@^7.0.2: terser "^5.0.0" rollup@^2.32.1, rollup@^2.34.0: - version "2.43.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.43.0.tgz#05d1ed0bbb37080a63e68c530a84d34f61ceb56a" - integrity sha512-FRsYGqlo1iF/w3bv319iStAK0hyhhwon35Cbo7sGUoXaOpsZFy6Lel7UoGb5bNDE4OsoWjMH94WiVvpOM26l3g== + version "2.44.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.44.0.tgz#8da324d1c4fd12beef9ae6e12f4068265b6d95eb" + integrity sha512-rGSF4pLwvuaH/x4nAS+zP6UNn5YUDWf/TeEU5IoXSZKBbKRNTCI3qMnYXKZgrC0D2KzS2baiOZt1OlqhMu5rnQ== optionalDependencies: fsevents "~2.3.1" @@ -2043,12 +2058,17 @@ symbol-tree@^3.2.4: integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== table@^6.0.4: - version "6.0.7" - resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" - integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== + version "6.0.8" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.8.tgz#b63f35af8d90601de282a3292226007a9429644f" + integrity sha512-OBAdezyozae8IvjHGXBDHByVkLCcsmffXUSj8LXkNb0SluRd4ug3GFCjk6JynZONIPhOkyr0Nnvbq1rlIspXyQ== dependencies: - ajv "^7.0.2" - lodash "^4.17.20" + ajv "^8.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + lodash.clonedeep "^4.5.0" + lodash.flatten "^4.4.0" + lodash.truncate "^4.4.2" slice-ansi "^4.0.0" string-width "^4.2.0" @@ -2278,7 +2298,7 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== -whatwg-url@^8.0.0: +whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.5.0.tgz#7752b8464fc0903fec89aa9846fc9efe07351fd3" integrity sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==