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.

122 lines (121 loc) 3.64 kB
import { bin as d3Bin } from "d3-array"; import { materializeGroups, toArray, transformValues } from "./transform-internal.js"; import { assertTransformOutputNames, prepareOutputs, reducePreparedOutputs } from "./transform-reduce-internal.js"; function binXY(source, options) { const data = toArray(source); const xValues = transformValues(data, options.x); const yValues = transformValues(data, options.y); const valid = data.flatMap( (datum, index) => isFiniteNumber(xValues[index]) && isFiniteNumber(yValues[index]) ? [ { datum, index, x: xValues[index], y: yValues[index] } ] : [] ); const rowByIndex = new Map(valid.map((row) => [row.index, row])); const xHistogram = histogram( options.xThresholds, options.xDomain, (row) => row.x ); const yHistogram = histogram( options.yThresholds, options.yDomain, (row) => row.y ); const xTemplate = xHistogram(valid); const yTemplate = yHistogram(valid); const outputs = options.outputs ?? { value: { reduce: "count" } }; const groups = materializeGroups(data, options.by); assertTransformOutputNames( outputs, [ ...Object.keys(groups[0]?.group ?? {}), "x", "x1", "x2", "y", "y1", "y2", "source", "sourceIndexes" ], "binXY" ); const prepared = prepareOutputs(data, outputs); return groups.flatMap(({ group, indexes }) => { const groupRows = indexes.flatMap((index) => { const row = rowByIndex.get(index); return row ? [row] : []; }); const cellIndexes = /* @__PURE__ */ new Map(); for (const row of groupRows) { const xPosition = intervalIndex(xTemplate, row.x); const yPosition = intervalIndex(yTemplate, row.y); if (xPosition < 0 || yPosition < 0) continue; const identity = `${xPosition}:${yPosition}`; const cell = cellIndexes.get(identity); if (cell) cell.push(row.index); else cellIndexes.set(identity, [row.index]); } return xTemplate.flatMap( (xEntry, xPosition) => yTemplate.map((yEntry, yPosition) => { const sourceIndexes = cellIndexes.get(`${xPosition}:${yPosition}`) ?? []; const x1 = xEntry.x0; const x2 = xEntry.x1; const y1 = yEntry.x0; const y2 = yEntry.x1; return { ...group, x: (x1 + x2) / 2, x1, x2, y: (y1 + y2) / 2, y1, y2, source: sourceIndexes.map((index) => data[index]), sourceIndexes, ...reducePreparedOutputs( data, sourceIndexes, group, prepared ) }; }) ); }); } function intervalIndex(bins, value) { return bins.findIndex( (entry, index) => entry.x0 !== void 0 && entry.x1 !== void 0 && value >= entry.x0 && (value < entry.x1 || index === bins.length - 1 && value === entry.x1) ); } function histogram(thresholds, domain, value) { const result = d3Bin().value(value); if (domain) result.domain([Math.min(...domain), Math.max(...domain)]); if (Array.isArray(thresholds)) { const boundaries = [...thresholds].sort((a, b) => a - b); if (boundaries.length < 2) throw new TypeError("binXY: boundary sequences require two values"); result.domain([boundaries[0], boundaries.at(-1)]).thresholds(boundaries.slice(1, -1)); } else if (typeof thresholds === "number") result.thresholds(thresholds); return result; } function isFiniteNumber(value) { return typeof value === "number" && Number.isFinite(value); } export { binXY };