Skip to content

Commit

Permalink
Precompute scaled channels. (observablehq#280)
Browse files Browse the repository at this point in the history
* precompute scaled channels

* comments

* cleaner, rule

* fully precomputed

* nullsafe scale

* comment

* penguin-species-island-sex example plot (observablehq#281)

* update dependencies

Co-authored-by: Philippe Rivière <fil@rezo.net>
  • Loading branch information
mbostock and Fil authored Mar 29, 2021
1 parent a47168b commit b31e739
Show file tree
Hide file tree
Showing 19 changed files with 303 additions and 126 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 6 additions & 7 deletions src/facet.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]};
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions src/mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
14 changes: 7 additions & 7 deletions src/marks/area.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,23 @@ 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)
.call(g => g.selectAll()
.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();
}
Expand Down
29 changes: 14 additions & 15 deletions src/marks/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]));
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
) {
Expand All @@ -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();
}
Expand Down
10 changes: 5 additions & 5 deletions src/marks/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,21 @@ 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)
.call(g => g.selectAll()
.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();
}
Expand Down
12 changes: 6 additions & 6 deletions src/marks/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
}
Expand Down
14 changes: 7 additions & 7 deletions src/marks/rect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
Expand Down
24 changes: 12 additions & 12 deletions src/marks/rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
) {
Expand All @@ -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();
}
Expand Down Expand Up @@ -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}
) {
Expand All @@ -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();
}
Expand Down
18 changes: 9 additions & 9 deletions src/marks/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
) {
Expand All @@ -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();
Expand Down
Loading

0 comments on commit b31e739

Please sign in to comment.