| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- 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;
- }
|