Skip to content

Commit 87c8e42

Browse files
committed
mark initializers
1 parent 66142e2 commit 87c8e42

File tree

13 files changed

+423
-120
lines changed

13 files changed

+423
-120
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@rollup/plugin-json": "4",
3939
"@rollup/plugin-node-resolve": "13",
4040
"canvas": "2",
41+
"d3-hexbin": "^0.2.2",
4142
"eslint": "8",
4243
"htl": "0.3",
4344
"js-beautify": "1",

src/channel.js

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@ import {first, labelof, maybeValue, range, valueof} from "./options.js";
33
import {registry} from "./scales/index.js";
44
import {maybeReduce} from "./transforms/group.js";
55

6+
export function channelObject(channelDescriptors, data) {
7+
const channels = {};
8+
for (const channel of channelDescriptors) {
9+
channels[channel.name] = Channel(data, channel);
10+
}
11+
return channels;
12+
}
13+
14+
// TODO use Float64Array.from for position and radius scales?
15+
export function valueObject(channels, scales) {
16+
const values = {};
17+
for (const channelName in channels) {
18+
const {scale: scaleName, value} = channels[channelName];
19+
const scale = scales[scaleName];
20+
values[channelName] = scale === undefined ? value : Array.from(value, scale);
21+
}
22+
return values;
23+
}
24+
625
// TODO Type coercion?
726
export function Channel(data, {scale, type, value, filter, hint}) {
827
return {
@@ -15,19 +34,22 @@ export function Channel(data, {scale, type, value, filter, hint}) {
1534
};
1635
}
1736

37+
// Note: mutates channel.domain! This is set to a function so that it is lazily
38+
// computed; i.e., if the scale’s domain is set explicitly, that takes priority
39+
// over the sort option, and we don’t need to do additional work.
1840
export function channelSort(channels, facetChannels, data, options) {
1941
const {reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options;
2042
for (const x in options) {
2143
if (!registry.has(x)) continue; // ignore unknown scale keys
2244
let {value: y, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]);
2345
if (reverse === undefined) reverse = y === "width" || y === "height"; // default to descending for lengths
2446
if (reduce == null || reduce === false) continue; // disabled reducer
25-
const X = channels.find(([, {scale}]) => scale === x) || facetChannels && facetChannels.find(([, {scale}]) => scale === x);
47+
const X = findScaleChannel(channels, x) || facetChannels && findScaleChannel(facetChannels, x);
2648
if (!X) throw new Error(`missing channel for scale: ${x}`);
27-
const XV = X[1].value;
49+
const XV = X.value;
2850
const [lo = 0, hi = Infinity] = limit && typeof limit[Symbol.iterator] === "function" ? limit : limit < 0 ? [limit] : [0, limit];
2951
if (y == null) {
30-
X[1].domain = () => {
52+
X.domain = () => {
3153
let domain = XV;
3254
if (reverse) domain = domain.slice().reverse();
3355
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
@@ -39,7 +61,7 @@ export function channelSort(channels, facetChannels, data, options) {
3961
: y === "width" ? difference(channels, "x1", "x2")
4062
: values(channels, y, y === "y" ? "y2" : y === "x" ? "x2" : undefined);
4163
const reducer = maybeReduce(reduce === true ? "max" : reduce, YV);
42-
X[1].domain = () => {
64+
X.domain = () => {
4365
let domain = rollup(range(XV), I => reducer.reduce(I, YV), i => XV[i]);
4466
domain = sort(domain, reverse ? descendingGroup : ascendingGroup);
4567
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
@@ -49,16 +71,23 @@ export function channelSort(channels, facetChannels, data, options) {
4971
}
5072
}
5173

74+
function findScaleChannel(channels, scale) {
75+
for (const name in channels) {
76+
const channel = channels[name];
77+
if (channel.scale === scale) return channel;
78+
}
79+
}
80+
5281
function difference(channels, k1, k2) {
5382
const X1 = values(channels, k1);
5483
const X2 = values(channels, k2);
5584
return Float64Array.from(X2, (x2, i) => Math.abs(x2 - X1[i]));
5685
}
5786

5887
function values(channels, name, alias) {
59-
let channel = channels.find(([n]) => n === name);
60-
if (!channel && alias !== undefined) channel = channels.find(([n]) => n === alias);
61-
if (channel) return channel[1].value;
88+
let channel = channels[name];
89+
if (!channel && alias !== undefined) channel = channels[alias];
90+
if (channel) return channel.value;
6291
throw new Error(`missing channel: ${name}`);
6392
}
6493

src/options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const field = name => d => d[name];
2222
export const indexOf = (d, i) => i;
2323
export const identity = {transform: d => d};
2424
export const zero = () => 0;
25+
export const yes = () => true;
2526
export const string = x => x == null ? x : `${x}`;
2627
export const number = x => x == null ? x : +x;
2728
export const boolean = x => x == null ? x : !!x;

src/plot.js

Lines changed: 97 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {create, cross, difference, groups, InternMap, select} from "d3";
22
import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js";
3-
import {Channel, channelSort} from "./channel.js";
3+
import {Channel, channelObject, channelSort, valueObject} from "./channel.js";
44
import {defined} from "./defined.js";
55
import {Dimensions} from "./dimensions.js";
66
import {Legends, exposeLegends} from "./legends.js";
7-
import {arrayify, isOptions, keyword, range, second, where} from "./options.js";
8-
import {Scales, ScaleFunctions, autoScaleRange, applyScales, exposeScales} from "./scales.js";
7+
import {arrayify, isOptions, isScaleOptions, keyword, range, second, where, yes} from "./options.js";
8+
import {Scales, ScaleFunctions, autoScaleRange, exposeScales} from "./scales.js";
9+
import {registry as scaleRegistry} from "./scales/index.js";
910
import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js";
1011
import {basic} from "./transforms/basic.js";
1112
import {consumeWarnings} from "./warnings.js";
@@ -29,25 +30,35 @@ export function plot(options = {}) {
2930
// A Map from scale name to an array of associated channels.
3031
const channelsByScale = new Map();
3132

33+
// If a scale is explicitly declared in options, initialize its associated
34+
// channels to the empty array; this will guarantee that a corresponding scale
35+
// will be created later (even if there are no other channels). But ignore
36+
// facet scale declarations if faceting is not enabled.
37+
for (const key of scaleRegistry.keys()) {
38+
if (isScaleOptions(options[key]) && key !== "fx" && key !== "fy") {
39+
channelsByScale.set(key, []);
40+
}
41+
}
42+
3243
// Faceting!
3344
let facets; // array of facet definitions (e.g. [["foo", [0, 1, 3, …]], …])
3445
let facetIndex; // index over the facet data, e.g. [0, 1, 2, 3, …]
35-
let facetChannels; // e.g. [["fx", {value}], ["fy", {value}]]
46+
let facetChannels; // e.g. {fx: {value}, fy: {value}}
3647
let facetsIndex; // nested array of facet indexes [[0, 1, 3, …], [2, 5, …], …]
3748
let facetsExclude; // lazily-constructed opposite of facetsIndex
3849
if (facet !== undefined) {
3950
const {x, y} = facet;
4051
if (x != null || y != null) {
4152
const facetData = arrayify(facet.data);
42-
facetChannels = [];
53+
facetChannels = {};
4354
if (x != null) {
4455
const fx = Channel(facetData, {value: x, scale: "fx"});
45-
facetChannels.push(["fx", fx]);
56+
facetChannels.fx = fx;
4657
channelsByScale.set("fx", [fx]);
4758
}
4859
if (y != null) {
4960
const fy = Channel(facetData, {value: y, scale: "fy"});
50-
facetChannels.push(["fy", fy]);
61+
facetChannels.fy = fy;
5162
channelsByScale.set("fy", [fy]);
5263
}
5364
facetIndex = range(facetData);
@@ -56,33 +67,20 @@ export function plot(options = {}) {
5667
}
5768
}
5869

59-
// Initialize the marks’ channels, indexing them by mark and scale as needed.
70+
// Initialize the marks’ state.
6071
for (const mark of marks) {
6172
if (stateByMark.has(mark)) throw new Error("duplicate mark");
62-
const markFacets = facets === undefined ? undefined
73+
const markFacets = facetsIndex === undefined ? undefined
6374
: mark.facet === "auto" ? mark.data === facet.data ? facetsIndex : undefined
6475
: mark.facet === "include" ? facetsIndex
6576
: mark.facet === "exclude" ? facetsExclude || (facetsExclude = facetsIndex.map(f => Uint32Array.from(difference(facetIndex, f))))
6677
: undefined;
67-
const {index, channels} = mark.initialize(markFacets, facetChannels);
68-
for (const [, channel] of channels) {
69-
const {scale} = channel;
70-
if (scale !== undefined) {
71-
const channels = channelsByScale.get(scale);
72-
if (channels !== undefined) channels.push(channel);
73-
else channelsByScale.set(scale, [channel]);
74-
}
75-
}
76-
stateByMark.set(mark, {index, channels, faceted: markFacets !== undefined});
77-
}
78-
79-
// Apply scale transforms, mutating channel.value.
80-
for (const [scale, channels] of channelsByScale) {
81-
const {percent, transform = percent ? x => x * 100 : undefined} = options[scale] || {};
82-
if (transform != null) for (const c of channels) c.value = Array.from(c.value, transform);
78+
const {facets, channels} = mark.initialize(markFacets, facetChannels);
79+
stateByMark.set(mark, {facets, channels: applyScaleTransforms(channels, options)});
8380
}
8481

85-
const scaleDescriptors = Scales(channelsByScale, options);
82+
// Initalize the scales and axes.
83+
const scaleDescriptors = Scales(addScaleChannels(channelsByScale, stateByMark), options);
8684
const scales = ScaleFunctions(scaleDescriptors);
8785
const axes = Axes(scaleDescriptors, options);
8886
const dimensions = Dimensions(scaleDescriptors, axes, options);
@@ -91,9 +89,30 @@ export function plot(options = {}) {
9189
autoScaleLabels(channelsByScale, scaleDescriptors, axes, dimensions, options);
9290
autoAxisTicks(scaleDescriptors, axes);
9391

92+
// Reinitialize; for deriving channels dependent on other channels.
93+
const newByScale = new Set();
94+
for (const [mark, state] of stateByMark) {
95+
if (mark.reinitialize != null) {
96+
const {facets, channels} = mark.reinitialize(state.facets, state.channels, scales);
97+
if (facets !== undefined) state.facets = facets;
98+
if (channels !== undefined) {
99+
Object.assign(state.channels, applyScaleTransforms(channels, options));
100+
for (const name in channels) newByScale.add(channels[name].scale);
101+
}
102+
}
103+
}
104+
105+
// Reconstruct scales if new scaled channels were created during reinitialization.
106+
if (newByScale.size) {
107+
const newScaleDescriptors = Scales(addScaleChannels(new Map(), stateByMark, key => newByScale.has(key)), options);
108+
const newScales = ScaleFunctions(newScaleDescriptors);
109+
Object.assign(scaleDescriptors, newScaleDescriptors);
110+
Object.assign(scales, newScales);
111+
}
112+
94113
// Compute value objects, applying scales as needed.
95114
for (const state of stateByMark.values()) {
96-
state.values = applyScales(state.channels, scales);
115+
state.values = valueObject(state.channels, scales);
97116
}
98117

99118
const {width, height} = dimensions;
@@ -175,16 +194,16 @@ export function plot(options = {}) {
175194
.attr("transform", facetTranslate(fx, fy))
176195
.each(function(key) {
177196
const j = indexByFacet.get(key);
178-
for (const [mark, {channels, values, index, faceted}] of stateByMark) {
179-
const renderIndex = mark.filter(faceted ? index[j] : index, channels, values);
180-
const node = mark.render(renderIndex, scales, values, subdimensions);
197+
for (const [mark, {channels, values, facets}] of stateByMark) {
198+
const facet = facets ? mark.filter(facets[j] ?? facets[0], channels, values) : null;
199+
const node = mark.render(facet, scales, values, subdimensions);
181200
if (node != null) this.appendChild(node);
182201
}
183202
});
184203
} else {
185-
for (const [mark, {channels, values, index}] of stateByMark) {
186-
const renderIndex = mark.filter(index, channels, values);
187-
const node = mark.render(renderIndex, scales, values, dimensions);
204+
for (const [mark, {channels, values, facets}] of stateByMark) {
205+
const facet = facets ? mark.filter(facets[0], channels, values) : null;
206+
const node = mark.render(facet, scales, values, dimensions);
188207
if (node != null) svg.appendChild(node);
189208
}
190209
}
@@ -227,6 +246,7 @@ export class Mark {
227246
const {facet = "auto", sort, dx, dy, clip} = options;
228247
const names = new Set();
229248
this.data = data;
249+
this.reinitialize = options.initialize;
230250
this.sort = isOptions(sort) ? sort : null;
231251
this.facet = facet == null || facet === false ? null : keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]);
232252
const {transform} = basic(options);
@@ -249,25 +269,18 @@ export class Mark {
249269
this.dy = +dy || 0;
250270
this.clip = maybeClip(clip);
251271
}
252-
initialize(facetIndex, facetChannels) {
272+
initialize(facets, facetChannels) {
253273
let data = arrayify(this.data);
254-
let index = facetIndex === undefined && data != null ? range(data) : facetIndex;
255-
if (data !== undefined && this.transform !== undefined) {
256-
if (facetIndex === undefined) index = index.length ? [index] : [];
257-
({facets: index, data} = this.transform(data, index));
258-
data = arrayify(data);
259-
if (facetIndex === undefined && index.length) ([index] = index);
260-
}
261-
const channels = this.channels.map(channel => {
262-
const {name} = channel;
263-
return [name == null ? undefined : `${name}`, Channel(data, channel)];
264-
});
274+
if (facets === undefined && data != null) facets = [range(data)];
275+
if (this.transform != null) ({facets, data} = this.transform(data, facets)), data = arrayify(data);
276+
const channels = channelObject(this.channels, data);
265277
if (this.sort != null) channelSort(channels, facetChannels, data, this.sort);
266-
return {index, channels};
278+
return {facets, channels};
267279
}
268280
filter(index, channels, values) {
269-
for (const [name, {filter = defined}] of channels) {
270-
if (name !== undefined && filter !== null) {
281+
for (const name in channels) {
282+
const {filter = defined} = channels[name];
283+
if (filter !== null) {
271284
const value = values[name];
272285
index = index.filter(i => filter(value[i]));
273286
}
@@ -298,6 +311,34 @@ class Render extends Mark {
298311
render() {}
299312
}
300313

314+
// Note: mutates channel.value to apply the scale transform, if any.
315+
function applyScaleTransforms(channels, options) {
316+
for (const name in channels) {
317+
const channel = channels[name];
318+
const {scale} = channel;
319+
if (scale != null) {
320+
const {percent, transform = percent ? x => x * 100 : undefined} = options[scale] || {};
321+
if (transform != null) channel.value = Array.from(channel.value, transform);
322+
}
323+
}
324+
return channels;
325+
}
326+
327+
function addScaleChannels(channelsByScale, stateByMark, filter = yes) {
328+
for (const {channels} of stateByMark.values()) {
329+
for (const name in channels) {
330+
const channel = channels[name];
331+
const {scale} = channel;
332+
if (scale != null && filter(scale)) {
333+
const channels = channelsByScale.get(scale);
334+
if (channels !== undefined) channels.push(channel);
335+
else channelsByScale.set(scale, [channel]);
336+
}
337+
}
338+
}
339+
return channelsByScale;
340+
}
341+
301342
// Derives a copy of the specified axis with the label disabled.
302343
function nolabel(axis) {
303344
return axis === undefined || axis.label === undefined
@@ -316,15 +357,17 @@ function facetKeys({fx, fy}) {
316357
// Returns an array of [[key1, index1], [key2, index2], …] representing the data
317358
// indexes associated with each facet. For two-dimensional faceting, each key
318359
// is a two-element array; see also facetMap.
319-
function facetGroups(index, channels) {
320-
return (channels.length > 1 ? facetGroup2 : facetGroup1)(index, ...channels);
360+
function facetGroups(index, {fx, fy}) {
361+
return fx && fy ? facetGroup2(index, fx, fy)
362+
: fx ? facetGroup1(index, fx)
363+
: facetGroup1(index, fy);
321364
}
322365

323-
function facetGroup1(index, [, {value: F}]) {
366+
function facetGroup1(index, {value: F}) {
324367
return groups(index, i => F[i]);
325368
}
326369

327-
function facetGroup2(index, [, {value: FX}], [, {value: FY}]) {
370+
function facetGroup2(index, {value: FX}, {value: FY}) {
328371
return groups(index, i => FX[i], i => FY[i])
329372
.flatMap(([x, xgroup]) => xgroup
330373
.map(([y, ygroup]) => [[x, y], ygroup]));
@@ -337,8 +380,8 @@ function facetTranslate(fx, fy) {
337380
: ky => `translate(0,${fy(ky)})`;
338381
}
339382

340-
function facetMap(channels) {
341-
return new (channels.length > 1 ? FacetMap2 : FacetMap);
383+
function facetMap({fx, fy}) {
384+
return new (fx && fy ? FacetMap2 : FacetMap);
342385
}
343386

344387
class FacetMap {

0 commit comments

Comments
 (0)