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.

145 lines (144 loc) 4.58 kB
import { bin as d3Bin } from "d3-array"; import { } from "./transform.js"; import { materializeGroups, toArray, transformValues } from "./transform-internal.js"; import { assertTransformOutputNames, prepareOutputs, reducePreparedOutputs } from "./transform-reduce-internal.js"; function binX(source, options) { return bins(source, options).map(({ lower, upper, ...datum }) => ({ ...datum, x: (lower + upper) / 2, x1: lower, x2: upper })); } function binY(source, options) { return bins(source, options).map(({ lower, upper, ...datum }) => ({ ...datum, y: (lower + upper) / 2, y1: lower, y2: upper })); } function bins(source, options) { const data = toArray(source); const values = transformValues(data, options.value); const rowsByIndex = []; const rows = data.flatMap((datum, index) => { const value = values[index]; if (!isFiniteNumber(value)) return []; const row = { datum, index, value }; rowsByIndex[index] = row; return [row]; }); const groups = materializeGroups(data, options.by).map( ({ group, indexes }) => ({ group, rows: indexes.flatMap((index) => { const row = rowsByIndex[index]; return row ? [row] : []; }) }) ); const template = createHistogram(options)(rows); const first = template[0]; const last = template.at(-1); if (!first || !last || first.x0 === void 0 || last.x1 === void 0) { return []; } const lower = first.x0; const upper = last.x1; const thresholds = template.slice(0, -1).flatMap((entry) => entry.x1 === void 0 ? [] : [entry.x1]); const histogram = d3Bin().value((row) => row.value).domain([lower, upper]).thresholds(thresholds); const outputs = options.outputs ?? { value: { reduce: "count" } }; assertTransformOutputNames( outputs, [ ...Object.keys(groups[0]?.group ?? {}), "x", "x1", "x2", "y", "y1", "y2", "source", "sourceIndexes" ], "bin" ); const preparedOutputs = prepareOutputs(data, outputs); return groups.flatMap( ({ group, rows: groupRows }) => histogram(groupRows).map((entry) => { const indexes = entry.map((row) => row.index); const entryLower = entry.x0 ?? lower; const entryUpper = entry.x1 ?? entryLower; return { ...group, lower: entryLower, upper: entryUpper, source: indexes.map((index) => data[index]), sourceIndexes: indexes, ...reducePreparedOutputs(data, indexes, group, preparedOutputs) }; }) ); } function createHistogram(options) { const histogram = d3Bin().value((row) => row.value); if (Array.isArray(options.thresholds)) { const boundaries = [...new Set(options.thresholds)].filter(isFiniteNumber).sort((left, right) => left - right); if (boundaries.length < 2) { throw new TypeError( "bin: an explicit boundary sequence requires two values" ); } const domain = [boundaries[0], boundaries.at(-1)]; if (options.domain) { const configuredDomain = normalizedDomain(options.domain); if (configuredDomain[0] !== domain[0] || configuredDomain[1] !== domain[1]) { throw new TypeError( "bin: domain must match the first and last explicit boundaries" ); } } histogram.domain(domain); histogram.thresholds(boundaries.slice(1, -1)); } else if (typeof options.thresholds === "number") { if (!Number.isFinite(options.thresholds) || options.thresholds <= 0) { throw new TypeError("bin: thresholds must be a positive finite number"); } if (options.domain) histogram.domain(normalizedDomain(options.domain)); histogram.thresholds(Math.floor(options.thresholds)); } else if (typeof options.thresholds === "function") { const threshold = options.thresholds; histogram.thresholds( (values, minimum, maximum) => threshold(Array.from(values), minimum, maximum) ); } else if (options.domain) { histogram.domain(normalizedDomain(options.domain)); } return histogram; } function normalizedDomain(domain) { if (!domain.every(isFiniteNumber)) { throw new TypeError("bin: domain values must be finite numbers"); } if (domain[0] < domain[1]) return [domain[0], domain[1]]; if (domain[1] < domain[0]) return [domain[1], domain[0]]; const padding = Math.abs(domain[0]) * 0.05 || 1; return [domain[0] - padding, domain[1] + padding]; } function isFiniteNumber(value) { return typeof value === "number" && Number.isFinite(value); } export { binX, binY };