UNPKG

@tanstack/charts

Version:

A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.

159 lines (158 loc) 5.37 kB
import { validateInferredLogDomain } from "./scale-input.js"; function createColorScale(values, options, theme) { if (options?.scale) { const infer = isColorScaleFactory(options.scale); const source = infer ? options.scale() : options.scale; if (typeof source !== "function" || typeof source.copy !== "function") { throw new TypeError("A color scale must be callable and copyable"); } if (infer && (typeof source.domain !== "function" || typeof source.range !== "function")) { throw new TypeError( "A color scale factory must return a scale with domain and range methods" ); } const scale = source.copy(); const kind = colorScaleKind(scale); if (infer) { const inferable = scale; if (options.range?.length) { inferable.range(options.range); } const domain3 = options.domain ?? inferColorDomain(inferable, values); if (options.domain !== void 0 || domain3.length) { inferable.domain(domain3); } const range3 = inferable.range(); if (!range3.length || range3.some((value) => typeof value !== "string")) { throw new TypeError("A color-scale factory requires a string range"); } } if (options.nice) { const nice = scale.nice; if (typeof nice !== "function") { throw new TypeError("This color scale does not support nicening"); } nice.call(scale, typeof options.nice === "number" ? options.nice : 5); } const domain2 = scale.domain?.() ?? options.domain ?? []; const range2 = (scale.range?.() ?? options.range ?? theme.palette).map( String ); return { type: "configured", kind, domain: domain2, range: range2, map: (value) => { if (value == null) return range2[0] ?? "currentColor"; const output = scale(value); return output == null ? "currentColor" : String(output); } }; } if (options?.resolver) { return options.resolver.resolve({ values, domain: options.domain, range: options.range, theme }); } const range = options?.range?.length ? options.range : theme.palette; const domain = uniqueChartKeys(options?.domain ?? values); const mappedKeys = domain.map(valueKey); const map = (value) => { if (value == null) return range[0] ?? "currentColor"; let index = mappedKeys.indexOf(valueKey(value)); if (index < 0) index = mappedKeys.push(valueKey(value)) - 1; return range[index % range.length] ?? "currentColor"; }; return { type: "ordinal", kind: "categorical", domain, range, map }; } function uniqueChartKeys(values) { return [...new Set(values.filter(isChartKey))]; } function isChartKey(value) { return typeof value === "string" || typeof value === "number"; } function isColorScaleFactory(source) { return typeof source === "function" && !("copy" in source); } function inferColorDomain(scale, values) { const observed = values.filter(isChartKey); const quantiles = scale.quantiles; const thresholds = scale.thresholds; if (quantiles) { return quantitativeColorValues(observed); } if (scale.invertExtent && !thresholds) { throw new TypeError( "Threshold color-scale factory requires an explicit domain" ); } if (scale.ticks || thresholds) { const numeric = quantitativeColorValues(observed); let minimum = Infinity; let maximum = -Infinity; for (const value of numeric) { minimum = Math.min(minimum, value); maximum = Math.max(maximum, value); } if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) { return []; } validateInferredLogDomain(scale, minimum, maximum); if (minimum === maximum) { if (minimum === 0) { maximum = 1; } else { const offset = Math.abs(minimum) * 0.05 || 1; minimum -= offset; maximum += offset; } } if (thresholds) return [minimum, maximum]; const stopCount = Math.max(2, scale.domain().length, scale.range().length); return Array.from( { length: stopCount }, (_value, index) => minimum + (maximum - minimum) * index / (stopCount - 1) ); } return uniqueChartKeys(observed); } function quantitativeColorValues(values) { const numeric = values.filter( (value) => typeof value === "number" && Number.isFinite(value) ); if (numeric.length !== values.length) colorScaleTypeMismatch(); return numeric; } function colorScaleTypeMismatch() { throw new TypeError( "A quantitative color-scale factory requires numeric values" ); } function colorScaleKind(scale) { if (scale.quantiles) { return scale.invertExtent ? "quantile" : "continuous"; } if (scale.thresholds) return "quantize"; if (scale.invertExtent) return "threshold"; return scale.ticks ? "continuous" : "categorical"; } function valueKey(value) { if (value instanceof Date) return `date:${value.getTime()}`; if (typeof value === "string") return `string:${value.length}:${value}`; return `${typeof value}:${String(value)}`; } function compareChartKey(left, right) { const leftOrder = `${typeof left}:${String(left)}`; const rightOrder = `${typeof right}:${String(right)}`; return leftOrder < rightOrder ? -1 : leftOrder > rightOrder ? 1 : 0; } export { compareChartKey, createColorScale, isChartKey, valueKey };