@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
JavaScript
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
};