-
Notifications
You must be signed in to change notification settings - Fork 187
/
Copy pathchannel.js
187 lines (174 loc) · 6.89 KB
/
channel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import {InternSet, rollups} from "d3";
import {ascendingDefined, descendingDefined} from "./defined.js";
import {first, isColor, isEvery, isIterable, isOpacity, labelof, map, maybeValue, range, valueof} from "./options.js";
import {registry} from "./scales/index.js";
import {isSymbol, maybeSymbol} from "./symbol.js";
import {maybeReduce} from "./transforms/group.js";
export function createChannel(data, {scale, type, value, filter, hint, label = labelof(value)}, name) {
if (hint === undefined && typeof value?.transform === "function") hint = value.hint;
return inferChannelScale(name, {
scale,
type,
value: valueof(data, value),
label,
filter,
hint
});
}
export function createChannels(channels, data) {
return Object.fromEntries(
Object.entries(channels).map(([name, channel]) => [name, createChannel(data, channel, name)])
);
}
// TODO Use Float64Array for scales with numeric ranges, e.g. position?
export function valueObject(channels, scales) {
const values = Object.fromEntries(
Object.entries(channels).map(([name, {scale: scaleName, value}]) => {
const scale = scaleName == null ? null : scales[scaleName];
return [name, scale == null ? value : map(value, scale)];
})
);
values.channels = channels; // expose channel state for advanced usage
return values;
}
// If the channel uses the "auto" scale (or equivalently true), infer the scale
// from the channel name and the provided values. For color and symbol channels,
// no scale is applied if the values are literal; however for symbols, we must
// promote symbol names (e.g., "plus") to symbol implementations (symbolPlus).
// Note: mutates channel!
export function inferChannelScale(name, channel) {
const {scale, value} = channel;
if (scale === true || scale === "auto") {
switch (name) {
case "fill":
case "stroke":
case "color":
channel.scale = scale !== true && isEvery(value, isColor) ? null : "color";
channel.defaultScale = "color";
break;
case "fillOpacity":
case "strokeOpacity":
case "opacity":
channel.scale = scale !== true && isEvery(value, isOpacity) ? null : "opacity";
channel.defaultScale = "opacity";
break;
case "symbol":
if (scale !== true && isEvery(value, isSymbol)) {
channel.scale = null;
channel.value = map(value, maybeSymbol);
} else {
channel.scale = "symbol";
}
channel.defaultScale = "symbol";
break;
default:
channel.scale = registry.has(name) ? name : null;
break;
}
} else if (scale === false) {
channel.scale = null;
} else if (scale != null && !registry.has(scale)) {
throw new Error(`unknown scale: ${scale}`);
}
return channel;
}
// Note: mutates channel.domain! This is set to a function so that it is lazily
// computed; i.e., if the scale’s domain is set explicitly, that takes priority
// over the sort option, and we don’t need to do additional work.
export function channelDomain(data, facets, channels, facetChannels, options) {
const {order: defaultOrder, reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options;
for (const x in options) {
if (!registry.has(x)) continue; // ignore unknown scale keys (including generic options)
let {value: y, order = defaultOrder, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]); // prettier-ignore
const negate = y?.startsWith("-");
if (negate) y = y.slice(1);
order = order === undefined ? negate !== (y === "width" || y === "height") ? descendingGroup : ascendingGroup : maybeOrder(order); // prettier-ignore
if (reduce == null || reduce === false) continue; // disabled reducer
const X = x === "fx" || x === "fy" ? reindexFacetChannel(facets, facetChannels[x]) : findScaleChannel(channels, x);
if (!X) throw new Error(`missing channel for scale: ${x}`);
const XV = X.value;
const [lo = 0, hi = Infinity] = isIterable(limit) ? limit : limit < 0 ? [limit] : [0, limit];
if (y == null) {
X.domain = () => {
let domain = Array.from(new InternSet(XV)); // remove any duplicates
if (reverse) domain = domain.reverse();
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
return domain;
};
} else {
const YV =
y === "data"
? data
: y === "height"
? difference(channels, "y1", "y2")
: y === "width"
? difference(channels, "x1", "x2")
: values(channels, y, y === "y" ? "y2" : y === "x" ? "x2" : undefined);
const reducer = maybeReduce(reduce === true ? "max" : reduce, YV);
X.domain = () => {
let domain = rollups(
range(XV),
(I) => reducer.reduceIndex(I, YV),
(i) => XV[i]
);
if (order) domain.sort(order);
if (reverse) domain.reverse();
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
return domain.map(first);
};
}
}
}
function findScaleChannel(channels, scale) {
for (const name in channels) {
const channel = channels[name];
if (channel.scale === scale) return channel;
}
}
// Facet channels are not affected by transforms; so, to compute the domain of a
// facet scale, we must first re-index the facet channel according to the
// transformed mark index. Note: mutates channel, but that should be safe here?
function reindexFacetChannel(facets, channel) {
const originalFacets = facets.original;
if (originalFacets === facets) return channel; // not transformed
const V1 = channel.value;
const V2 = (channel.value = []); // mutates channel!
for (let i = 0; i < originalFacets.length; ++i) {
const vi = V1[originalFacets[i][0]];
for (const j of facets[i]) V2[j] = vi;
}
return channel;
}
function difference(channels, k1, k2) {
const X1 = values(channels, k1);
const X2 = values(channels, k2);
return map(X2, (x2, i) => Math.abs(x2 - X1[i]), Float64Array);
}
function values(channels, name, alias) {
let channel = channels[name];
if (!channel && alias !== undefined) channel = channels[alias];
if (channel) return channel.value;
throw new Error(`missing channel: ${name}`);
}
function maybeOrder(order) {
if (order == null || typeof order === "function") return order;
switch (`${order}`.toLowerCase()) {
case "ascending":
return ascendingGroup;
case "descending":
return descendingGroup;
}
throw new Error(`invalid order: ${order}`);
}
function ascendingGroup([ak, av], [bk, bv]) {
return ascendingDefined(av, bv) || ascendingDefined(ak, bk);
}
function descendingGroup([ak, av], [bk, bv]) {
return descendingDefined(av, bv) || ascendingDefined(ak, bk);
}
export function getSource(channels, key) {
let channel = channels[key];
if (!channel) return;
while (channel.source) channel = channel.source;
return channel.source === null ? null : channel;
}