channel.js 6.9 KB

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