-
Notifications
You must be signed in to change notification settings - Fork 230
Expand file tree
/
Copy pathchannel.js
More file actions
140 lines (130 loc) · 4.98 KB
/
Copy pathchannel.js
File metadata and controls
140 lines (130 loc) · 4.98 KB
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
import {ascending, descending, rollup, sort} from "d3";
import {first, isColor, isEvery, isIterable, labelof, map, maybeValue, range, valueof} from "./options.js";
import {registry} from "./scales/index.js";
import {isSymbol, maybeSymbol} from "./symbols.js";
import {maybeReduce} from "./transforms/group.js";
// TODO Type coercion?
export function Channel(data, {scale, type, value, filter, hint}, name) {
return inferChannelScale(name, {
scale,
type,
value: valueof(data, value),
label: labelof(value),
filter,
hint
});
}
export function Channels(channels, data) {
return Object.fromEntries(Object.entries(channels).map(([name, channel]) => [name, Channel(data, channel, name)]));
}
// TODO Use Float64Array for scales with numeric ranges, e.g. position?
export function valueObject(channels, scales) {
return 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)];
})
);
}
// 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 = isEvery(value, isColor) ? null : "color";
break;
case "fillOpacity":
case "strokeOpacity":
channel.scale = "opacity";
break;
case "symbol":
if (isEvery(value, isSymbol)) {
channel.scale = null;
channel.value = map(value, maybeSymbol);
} else {
channel.scale = "symbol";
}
break;
default:
channel.scale = registry.has(name) ? name : null;
break;
}
} 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(channels, facetChannels, data, options) {
const {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, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]);
if (reverse === undefined) reverse = y === "width" || y === "height"; // default to descending for lengths
if (reduce == null || reduce === false) continue; // disabled reducer
const X = findScaleChannel(channels, x) || (facetChannels && findScaleChannel(facetChannels, 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 = XV;
if (reverse) domain = domain.slice().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 = rollup(
range(XV),
(I) => reducer.reduce(I, YV),
(i) => XV[i]
);
domain = sort(domain, reverse ? descendingGroup : ascendingGroup);
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;
}
}
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 ascendingGroup([ak, av], [bk, bv]) {
return ascending(av, bv) || ascending(ak, bk);
}
function descendingGroup([ak, av], [bk, bv]) {
return descending(av, bv) || ascending(ak, bk);
}