scale.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. import { Linear, createInterpolateValue } from '@antv/scale';
  2. import { extent } from 'd3-array';
  3. import * as d3ScaleChromatic from 'd3-scale-chromatic';
  4. import { upperFirst } from '@antv/util';
  5. import { firstOf, lastOf, unique } from '../utils/array';
  6. import { defined, identity, isStrictObject } from '../utils/helper';
  7. import { isTheta } from './coordinate';
  8. import { useLibrary } from './library';
  9. export function inferScale(name, values, options, coordinates, theme, library) {
  10. const { guide = {} } = options;
  11. const type = inferScaleType(name, values, options);
  12. if (typeof type !== 'string')
  13. return options;
  14. const expectedDomain = inferScaleDomain(type, name, values, options);
  15. const actualDomain = maybeRatio(type, expectedDomain, options);
  16. return Object.assign(Object.assign(Object.assign({}, options), inferScaleOptions(type, name, values, options, coordinates)), { domain: actualDomain, range: inferScaleRange(type, name, values, options, actualDomain, theme, library), expectedDomain,
  17. guide,
  18. name,
  19. type });
  20. }
  21. export function applyScale(channels, scale) {
  22. const scaledValue = {};
  23. for (const channel of channels) {
  24. const { values, name: scaleName } = channel;
  25. const scaleInstance = scale[scaleName];
  26. for (const value of values) {
  27. const { name, value: V } = value;
  28. scaledValue[name] = V.map((d) => scaleInstance.map(d));
  29. }
  30. }
  31. return scaledValue;
  32. }
  33. export function useRelation(relations) {
  34. if (!relations || !Array.isArray(relations))
  35. return [identity, identity];
  36. // Store original map and invert.
  37. let map;
  38. let invert;
  39. const conditionalize = (scale) => {
  40. var _a;
  41. map = scale.map.bind(scale);
  42. invert = (_a = scale.invert) === null || _a === void 0 ? void 0 : _a.bind(scale);
  43. // Distinguish functions[function, output] and value[vale, output] relations.
  44. const funcRelations = relations.filter(([v]) => typeof v === 'function');
  45. const valueRelations = relations.filter(([v]) => typeof v !== 'function');
  46. // Update scale.map
  47. const valueOutput = new Map(valueRelations);
  48. scale.map = (x) => {
  49. for (const [verify, value] of funcRelations) {
  50. if (verify(x))
  51. return value;
  52. }
  53. if (valueOutput.has(x))
  54. return valueOutput.get(x);
  55. return map(x);
  56. };
  57. if (!invert)
  58. return scale;
  59. // Update scale.invert
  60. const outputValue = new Map(valueRelations.map(([a, b]) => [b, a]));
  61. const outputFunc = new Map(funcRelations.map(([a, b]) => [b, a]));
  62. scale.invert = (x) => {
  63. if (outputFunc.has(x))
  64. return x;
  65. if (outputValue.has(x))
  66. return outputValue.get(x);
  67. return invert(x);
  68. };
  69. return scale;
  70. };
  71. const deconditionalize = (scale) => {
  72. if (map !== null)
  73. scale.map = map;
  74. if (invert !== null)
  75. scale.invert = invert;
  76. return scale;
  77. };
  78. return [conditionalize, deconditionalize];
  79. }
  80. export function useRelationScale(options, library) {
  81. const [useScale] = useLibrary('scale', library);
  82. const { relations } = options;
  83. const [conditionalize] = useRelation(relations);
  84. const scale = useScale(options);
  85. return conditionalize(scale);
  86. }
  87. export function syncFacetsScales(states) {
  88. const scales = states
  89. .flatMap((d) => Array.from(d.values()))
  90. .flatMap((d) => d.channels.map((d) => d.scale));
  91. syncFacetsScaleByChannel(scales, 'x');
  92. syncFacetsScaleByChannel(scales, 'y');
  93. }
  94. function syncFacetsScaleByChannel(scales, channel) {
  95. const S = scales.filter(({ name, facet = true }) => facet && name === channel);
  96. const D = S.flatMap((d) => d.domain);
  97. const syncedD = S.every(isQuantitativeScale)
  98. ? extent(D)
  99. : S.every(isDiscreteScale)
  100. ? Array.from(new Set(D))
  101. : null;
  102. if (syncedD === null)
  103. return;
  104. for (const scale of S) {
  105. scale.domain = syncedD;
  106. }
  107. }
  108. function maybeRatio(type, domain, options) {
  109. const { ratio } = options;
  110. if (ratio === undefined || ratio === null)
  111. return domain;
  112. if (isQuantitativeScale({ type })) {
  113. return clampQuantitativeScale(domain, ratio, type);
  114. }
  115. if (isDiscreteScale({ type }))
  116. return clampDiscreteScale(domain, ratio);
  117. return domain;
  118. }
  119. function clampQuantitativeScale(domain, ratio, type) {
  120. const D = domain.map(Number);
  121. const scale = new Linear({
  122. domain: D,
  123. range: [D[0], D[0] + (D[D.length - 1] - D[0]) * ratio],
  124. });
  125. if (type === 'time')
  126. return domain.map((d) => new Date(scale.map(d)));
  127. return domain.map((d) => scale.map(d));
  128. }
  129. function clampDiscreteScale(domain, ratio) {
  130. const index = Math.round(domain.length * ratio);
  131. return domain.slice(0, index);
  132. }
  133. function isQuantitativeScale(scale) {
  134. const { type } = scale;
  135. if (typeof type !== 'string')
  136. return false;
  137. // Do not take quantize, quantile or threshold scale into account,
  138. // because they are not for position scales. If they are, there is
  139. // no need to sync them.
  140. const names = ['linear', 'log', 'pow', 'time'];
  141. return names.includes(type);
  142. }
  143. function isDiscreteScale(scale) {
  144. const { type } = scale;
  145. if (typeof type !== 'string')
  146. return false;
  147. const names = ['band', 'point', 'ordinal'];
  148. return names.includes(type);
  149. }
  150. // @todo More accurate inference for different cases.
  151. function inferScaleType(name, values, options) {
  152. const { type, domain, range } = options;
  153. if (type !== undefined)
  154. return type;
  155. if (isObject(values))
  156. return 'identity';
  157. if (typeof range === 'string')
  158. return 'linear';
  159. if ((domain || range || []).length > 2)
  160. return asOrdinalType(name);
  161. if (domain !== undefined) {
  162. if (isOrdinal([domain]))
  163. return asOrdinalType(name);
  164. if (isTemporal(values))
  165. return 'time';
  166. return asQuantitativeType(name, range);
  167. }
  168. if (isOrdinal(values))
  169. return asOrdinalType(name);
  170. if (isTemporal(values))
  171. return 'time';
  172. return asQuantitativeType(name, range);
  173. }
  174. function inferScaleDomain(type, name, values, options) {
  175. const { domain } = options;
  176. if (domain !== undefined)
  177. return domain;
  178. switch (type) {
  179. case 'linear':
  180. case 'time':
  181. case 'log':
  182. case 'pow':
  183. case 'sqrt':
  184. case 'quantize':
  185. case 'threshold':
  186. return maybeMinMax(inferDomainQ(values, options), options);
  187. case 'band':
  188. case 'ordinal':
  189. case 'point':
  190. return inferDomainC(values);
  191. case 'quantile':
  192. return inferDomainO(values);
  193. case 'sequential':
  194. return maybeMinMax(inferDomainS(values), options);
  195. default:
  196. return [];
  197. }
  198. }
  199. function inferScaleRange(type, name, values, options, domain, theme, library) {
  200. const { range } = options;
  201. if (typeof range === 'string')
  202. return gradientColors(range);
  203. if (range !== undefined)
  204. return range;
  205. const { rangeMin, rangeMax } = options;
  206. switch (type) {
  207. case 'linear':
  208. case 'time':
  209. case 'log':
  210. case 'pow':
  211. case 'sqrt': {
  212. const colors = categoricalColors(values, options, domain, theme, library);
  213. const [r0, r1] = inferRangeQ(name, colors);
  214. return [rangeMin || r0, rangeMax || r1];
  215. }
  216. case 'band':
  217. case 'point':
  218. return [rangeMin || 0, rangeMax || 1];
  219. case 'ordinal': {
  220. return categoricalColors(values, options, domain, theme, library);
  221. }
  222. case 'sequential':
  223. return undefined;
  224. case 'constant':
  225. return [values[0][0]];
  226. default:
  227. return [];
  228. }
  229. }
  230. function inferScaleOptions(type, name, values, options, coordinates) {
  231. switch (type) {
  232. case 'linear':
  233. case 'time':
  234. case 'log':
  235. case 'pow':
  236. case 'sqrt':
  237. return inferOptionsQ(coordinates, options);
  238. case 'band':
  239. case 'point':
  240. return inferOptionsC(type, name, coordinates, options);
  241. case 'sequential':
  242. return inferOptionsS(options);
  243. default:
  244. return options;
  245. }
  246. }
  247. function categoricalColors(values, options, domain, theme, library) {
  248. const [usePalette] = useLibrary('palette', library);
  249. const { defaultCategory10: c10, defaultCategory20: c20 } = theme;
  250. const defaultPalette = unique(values.flat()).length <= c10.length ? c10 : c20;
  251. const { palette = defaultPalette, offset } = options;
  252. // Built-in palettes have higher priority.
  253. try {
  254. return usePalette({ type: palette });
  255. }
  256. catch (e) {
  257. const colors = interpolatedColors(palette, domain, offset);
  258. if (colors)
  259. return colors;
  260. throw new Error(`Unknown Component: ${palette} `);
  261. }
  262. }
  263. function gradientColors(range) {
  264. return range.split('-');
  265. }
  266. function interpolatedColors(palette, domain, offset = (d) => d) {
  267. if (!palette)
  268. return null;
  269. const fullName = upperFirst(palette);
  270. // If scheme have enough colors, then return pre-defined colors.
  271. const scheme = d3ScaleChromatic[`scheme${fullName}`];
  272. const interpolator = d3ScaleChromatic[`interpolate${fullName}`];
  273. if (!scheme && !interpolator)
  274. return null;
  275. if (scheme) {
  276. // If is a one dimension array, return it.
  277. if (!scheme.some(Array.isArray))
  278. return scheme;
  279. const schemeColors = scheme[domain.length];
  280. if (schemeColors)
  281. return schemeColors;
  282. }
  283. // Otherwise interpolate to get full colors.
  284. return domain.map((_, i) => interpolator(offset(i / domain.length)));
  285. }
  286. function inferOptionsS(options) {
  287. const { palette = 'ylGnBu', offset } = options;
  288. const name = upperFirst(palette);
  289. const interpolator = d3ScaleChromatic[`interpolate${name}`];
  290. if (!interpolator)
  291. throw new Error(`Unknown palette: ${name}`);
  292. return {
  293. interpolator: offset ? (x) => interpolator(offset(x)) : interpolator,
  294. };
  295. }
  296. function inferOptionsQ(coordinates, options) {
  297. const { interpolate = createInterpolateValue, nice = false, tickCount = 5, } = options;
  298. return Object.assign(Object.assign({}, options), { interpolate, nice, tickCount });
  299. }
  300. function inferOptionsC(type, name, coordinates, options) {
  301. if (options.padding !== undefined ||
  302. options.paddingInner !== undefined ||
  303. options.paddingOuter !== undefined) {
  304. return Object.assign(Object.assign({}, options), { unknown: NaN });
  305. }
  306. const padding = inferPadding(type, name, coordinates);
  307. const { paddingInner = padding, paddingOuter = padding } = options;
  308. return Object.assign(Object.assign({}, options), { paddingInner,
  309. paddingOuter,
  310. padding, unknown: NaN });
  311. }
  312. function inferPadding(type, name, coordinates) {
  313. // The scale for enterDelay and enterDuration should has zero padding by default.
  314. // Because there is no need to add extra delay for the start and the end.
  315. if (name === 'enterDelay' || name === 'enterDuration')
  316. return 0;
  317. if (type === 'band') {
  318. return isTheta(coordinates) ? 0 : 0.1;
  319. }
  320. // Point scale need 0.5 padding to make interval between first and last point
  321. // equal to other intervals in polar coordinate.
  322. if (type === 'point')
  323. return 0.5;
  324. return 0;
  325. }
  326. function asOrdinalType(name) {
  327. return isQuantitative(name) ? 'point' : 'ordinal';
  328. }
  329. function asQuantitativeType(name, range) {
  330. if (name !== 'color')
  331. return 'linear';
  332. return range ? 'linear' : 'sequential';
  333. }
  334. function maybeMinMax(domain, options) {
  335. if (domain.length === 0)
  336. return domain;
  337. const { domainMin, domainMax } = options;
  338. const [d0, d1] = domain;
  339. return [domainMin !== null && domainMin !== void 0 ? domainMin : d0, domainMax !== null && domainMax !== void 0 ? domainMax : d1];
  340. }
  341. function inferDomainQ(values, options) {
  342. const { zero = false } = options;
  343. let min = Infinity;
  344. let max = -Infinity;
  345. for (const value of values) {
  346. for (const d of value) {
  347. if (defined(d)) {
  348. min = Math.min(min, +d);
  349. max = Math.max(max, +d);
  350. }
  351. }
  352. }
  353. if (min === Infinity)
  354. return [];
  355. return zero ? [Math.min(0, min), max] : [min, max];
  356. }
  357. function inferDomainC(values) {
  358. return Array.from(new Set(values.flat()));
  359. }
  360. function inferDomainO(values) {
  361. return inferDomainC(values).sort();
  362. }
  363. function inferDomainS(values) {
  364. let min = Infinity;
  365. let max = -Infinity;
  366. for (const value of values) {
  367. for (const d of value) {
  368. if (defined(d)) {
  369. min = Math.min(min, +d);
  370. max = Math.max(max, +d);
  371. }
  372. }
  373. }
  374. if (min === Infinity)
  375. return [];
  376. return [min < 0 ? -max : min, max];
  377. }
  378. /**
  379. * @todo More nice default range for enterDelay and enterDuration.
  380. * @todo Move these to channel definition.
  381. */
  382. function inferRangeQ(name, palette) {
  383. if (name === 'enterDelay')
  384. return [0, 1000];
  385. if (name == 'enterDuration')
  386. return [300, 1000];
  387. if (name.startsWith('y') || name.startsWith('position'))
  388. return [1, 0];
  389. if (name === 'color')
  390. return [firstOf(palette), lastOf(palette)];
  391. if (name === 'opacity')
  392. return [0, 1];
  393. if (name === 'size')
  394. return [1, 10];
  395. return [0, 1];
  396. }
  397. function isOrdinal(values) {
  398. return some(values, (d) => {
  399. const type = typeof d;
  400. return type === 'string' || type === 'boolean';
  401. });
  402. }
  403. function isTemporal(values) {
  404. return some(values, (d) => d instanceof Date);
  405. }
  406. function isObject(values) {
  407. return some(values, isStrictObject);
  408. }
  409. function some(values, callback) {
  410. for (const V of values) {
  411. if (V.some(callback))
  412. return true;
  413. }
  414. return false;
  415. }
  416. function isQuantitative(name) {
  417. return (name.startsWith('x') ||
  418. name.startsWith('y') ||
  419. name.startsWith('position') ||
  420. name.startsWith('size'));
  421. }
  422. // Spatial and temporal position.
  423. export function isPosition(name) {
  424. return (name.startsWith('x') ||
  425. name.startsWith('y') ||
  426. name.startsWith('position') ||
  427. name === 'enterDelay' ||
  428. name === 'enterDuration' ||
  429. name === 'updateDelay' ||
  430. name === 'updateDuration' ||
  431. name === 'exitDelay' ||
  432. name === 'exitDuration');
  433. }
  434. export function isValidScale(scale) {
  435. if (!scale || !scale.type)
  436. return false;
  437. if (typeof scale.type === 'function')
  438. return true;
  439. const { type, domain, range, interpolator } = scale;
  440. const isValidDomain = domain && domain.length > 0;
  441. const isValidRange = range && range.length > 0;
  442. if ([
  443. 'linear',
  444. 'sqrt',
  445. 'log',
  446. 'time',
  447. 'pow',
  448. 'threshold',
  449. 'quantize',
  450. 'quantile',
  451. 'ordinal',
  452. 'band',
  453. 'point',
  454. ].includes(type) &&
  455. isValidDomain &&
  456. isValidRange) {
  457. return true;
  458. }
  459. if (['sequential'].includes(type) &&
  460. isValidDomain &&
  461. (isValidRange || interpolator)) {
  462. return true;
  463. }
  464. if (['constant', 'identity'].includes(type) && isValidRange)
  465. return true;
  466. return false;
  467. }
  468. //# sourceMappingURL=scale.js.map