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.

402 lines (401 loc) 14.7 kB
import { scaleBand } from "d3-scale"; import { channelValues, createMark, inferredKeyValues, isChartKey, isChartValue, isFiniteNumber, markStates, visualValue } from "./mark.js"; import { resolveScaleInput } from "./scale-input.js"; import { valueKey } from "./scales.js"; import { stackValues } from "./stack-internal.js"; function barY(source, options = {}) { const data = Array.isArray(source) ? source : Array.from(source); return createMark(({ markIndex }) => { const id = options.id ?? `bar-y-${markIndex}`; const xValues = channelValues(data, options.x, (_datum, { index }) => index); const rawYValues = numericChannelValues( data, options.y ?? options.y2, (datum) => typeof datum === "number" ? datum : void 0 ); const zValues = channelValues(data, options.z, () => null); const colorValues = options.color === void 0 ? zValues : channelValues(data, options.color, () => null); const seriesValues = options.z === void 0 && options.color !== void 0 ? colorValues : zValues; const explicitExtent = options.y1 !== void 0 || options.y2 !== void 0; if (explicitExtent && options.layout?.type === "stack") { throw new TypeError( "A bar with explicit y1 or y2 endpoints cannot also configure a stack layout" ); } const grouped = options.layout?.type === "group"; const stackLayout = options.layout?.type === "stack" ? options.layout : {}; const stacked = !explicitExtent && !grouped ? stackValues(xValues, rawYValues, seriesValues, stackLayout, "index") : void 0; const y1Values = explicitExtent ? numericChannelValues(data, options.y1, () => 0) : stacked?.starts ?? data.map(() => 0); const y2Values = explicitExtent ? numericChannelValues(data, options.y2 ?? options.y, () => void 0) : grouped ? rawYValues : stacked.ends; const duplicatePositions = hasDuplicateValues(xValues); const groupValues = grouped || !explicitExtent && duplicatePositions ? seriesValues : zValues; const keys = inferredKeyValues(data, options.key, { groups: groupValues, candidates: [xValues], markId: id, warningIdentity: options }); return { id, states: markStates(data, options.states), seriesFromColor: options.z === void 0 && options.color !== void 0 && (grouped || duplicatePositions), channels: { x: { scale: "x", values: xValues.filter(isChartValue) }, y: { scale: "y", values: [ ...y2Values.filter(isFiniteNumber), ...y1Values.filter(isFiniteNumber) ], includeZero: options.y1 === void 0 }, color: { scale: "color", values: colorValues.filter(isChartKey) } }, render: ({ scales, chart, color: resolveColor }) => { const totalBandwidth = scales.x.bandwidth || inferBandwidth(scales.x, xValues, chart.width, data.length); const groupScale = resolveGroupScale( options.layout?.type === "group" ? options.layout : void 0, groupValues, totalBandwidth ); const groupBandwidth = groupScale?.bandwidth ?? totalBandwidth; const thickness = resolveBarThickness( groupBandwidth, options.inset, options.maxThickness ); const nodes = []; data.forEach((datum, datumIndex) => { const xValue = xValues[datumIndex]; const yValue = rawYValues[datumIndex]; const y1Value = y1Values[datumIndex]; const y2Value = y2Values[datumIndex]; if (!isChartValue(xValue) || !isFiniteNumber(yValue) || !isFiniteNumber(y1Value) || !isFiniteNumber(y2Value)) return; const group = groupValues[datumIndex] ?? null; const groupOffset = groupScale?.map(group) ?? 0; const resolvedColor = resolveColor(colorValues[datumIndex]); const fill = visualValue( options.fill, datum, datumIndex, data, resolvedColor ); const stroke = visualValue( options.stroke, datum, datumIndex, data, "none" ); const strokeDasharray = visualValue( options.strokeDasharray, datum, datumIndex, data, "none" ); const center = scales.x.map(xValue); const baselinePosition = scales.y.map(y1Value); const valuePosition = scales.y.map(y2Value); const x = center - totalBandwidth / 2 + groupOffset + thickness.inset; const y = Math.min(baselinePosition, valuePosition); const width = thickness.size; const height = Math.abs(baselinePosition - valuePosition); const key = `${id}:${valueKey(group)}:${valueKey(keys[datumIndex])}`; const point = { key, markId: id, group, groupLabel: group == null ? id : String(group), datum, datumIndex, xValue, yValue, y1Value, y2Value, yInterval: "difference", x: center - totalBandwidth / 2 + groupOffset + groupBandwidth / 2, y: valuePosition, color: fill }; nodes.push({ kind: "rect", key, x, y, width, height, radius: options.radius, inset: thickness.inset, insetAxis: "x", ...thickness.maximum === void 0 ? {} : { maxThickness: thickness.maximum }, interaction: { point, affinity: "x" }, style: { fill, fillOpacity: options.fillOpacity, stroke, strokeOpacity: options.strokeOpacity, strokeWidth: options.strokeWidth, strokeDasharray } }); }); return { nodes: [ { kind: "group", key: id, className: "ts-chart__bar ts-chart__bar-y", ariaHidden: true, children: nodes } ] }; } }; }, options.motion); } function barX(source, options = {}) { const data = Array.isArray(source) ? source : Array.from(source); return createMark(({ markIndex }) => { const id = options.id ?? `bar-x-${markIndex}`; const rawXValues = numericChannelValues( data, options.x ?? options.x2, (datum) => typeof datum === "number" ? datum : void 0 ); const yValues = channelValues(data, options.y, (_datum, { index }) => index); const zValues = channelValues(data, options.z, () => null); const colorValues = options.color === void 0 ? zValues : channelValues(data, options.color, () => null); const seriesValues = options.z === void 0 && options.color !== void 0 ? colorValues : zValues; const explicitExtent = options.x1 !== void 0 || options.x2 !== void 0; if (explicitExtent && options.layout?.type === "stack") { throw new TypeError( "A bar with explicit x1 or x2 endpoints cannot also configure a stack layout" ); } const grouped = options.layout?.type === "group"; const stackLayout = options.layout?.type === "stack" ? options.layout : {}; const stacked = !explicitExtent && !grouped ? stackValues(yValues, rawXValues, seriesValues, stackLayout, "index") : void 0; const x1Values = explicitExtent ? numericChannelValues(data, options.x1, () => 0) : stacked?.starts ?? data.map(() => 0); const x2Values = explicitExtent ? numericChannelValues(data, options.x2 ?? options.x, () => void 0) : grouped ? rawXValues : stacked.ends; const duplicatePositions = hasDuplicateValues(yValues); const groupValues = grouped || !explicitExtent && duplicatePositions ? seriesValues : zValues; const keys = inferredKeyValues(data, options.key, { groups: groupValues, candidates: [yValues], markId: id, warningIdentity: options }); return { id, states: markStates(data, options.states), seriesFromColor: options.z === void 0 && options.color !== void 0 && (grouped || duplicatePositions), channels: { x: { scale: "x", values: [ ...x2Values.filter(isFiniteNumber), ...x1Values.filter(isFiniteNumber) ], includeZero: options.x1 === void 0 }, y: { scale: "y", values: yValues.filter(isChartValue) }, color: { scale: "color", values: colorValues.filter(isChartKey) } }, render: ({ scales, chart, color: resolveColor }) => { const totalBandwidth = scales.y.bandwidth || inferBandwidth(scales.y, yValues, chart.height, data.length); const groupScale = resolveGroupScale( options.layout?.type === "group" ? options.layout : void 0, groupValues, totalBandwidth ); const groupBandwidth = groupScale?.bandwidth ?? totalBandwidth; const thickness = resolveBarThickness( groupBandwidth, options.inset, options.maxThickness ); const nodes = []; data.forEach((datum, datumIndex) => { const xValue = rawXValues[datumIndex]; const x1Value = x1Values[datumIndex]; const x2Value = x2Values[datumIndex]; const yValue = yValues[datumIndex]; if (!isFiniteNumber(xValue) || !isFiniteNumber(x1Value) || !isFiniteNumber(x2Value) || !isChartValue(yValue)) return; const group = groupValues[datumIndex] ?? null; const groupOffset = groupScale?.map(group) ?? 0; const resolvedColor = resolveColor(colorValues[datumIndex]); const fill = visualValue( options.fill, datum, datumIndex, data, resolvedColor ); const stroke = visualValue( options.stroke, datum, datumIndex, data, "none" ); const strokeDasharray = visualValue( options.strokeDasharray, datum, datumIndex, data, "none" ); const baselinePosition = scales.x.map(x1Value); const valuePosition = scales.x.map(x2Value); const center = scales.y.map(yValue); const y = center - totalBandwidth / 2 + groupOffset + thickness.inset; const x = Math.min(baselinePosition, valuePosition); const width = Math.abs(baselinePosition - valuePosition); const height = thickness.size; const key = `${id}:${valueKey(group)}:${valueKey(keys[datumIndex])}`; const point = { key, markId: id, group, groupLabel: group == null ? id : String(group), datum, datumIndex, xValue, yValue, x1Value, x2Value, xInterval: "difference", x: valuePosition, y: center - totalBandwidth / 2 + groupOffset + groupBandwidth / 2, color: fill }; nodes.push({ kind: "rect", key, x, y, width, height, radius: options.radius, inset: thickness.inset, insetAxis: "y", ...thickness.maximum === void 0 ? {} : { maxThickness: thickness.maximum }, interaction: { point, affinity: "y" }, style: { fill, fillOpacity: options.fillOpacity, stroke, strokeOpacity: options.strokeOpacity, strokeWidth: options.strokeWidth, strokeDasharray } }); }); return { nodes: [ { kind: "group", key: id, className: "ts-chart__bar ts-chart__bar-x", ariaHidden: true, children: nodes } ] }; } }; }, options.motion); } function resolveBarThickness(bandwidth, insetOption, maxThicknessOption) { const authoredInset = Math.max(0, insetOption ?? 0); const resolvedBandwidth = Math.max(0, bandwidth); const available = Math.max(0, resolvedBandwidth - authoredInset * 2); const constrained = Number.isFinite(maxThicknessOption); const maximum = constrained ? Math.max(0, maxThicknessOption) : available; const size = Math.min(available, maximum); return { inset: (resolvedBandwidth - size) / 2, maximum: constrained ? maximum : void 0, size }; } function resolveGroupScale(source, values, bandwidth) { if (!source) return void 0; const scale = resolveScaleInput( source.scale ?? (() => scaleBand().padding( Number.isFinite(source.padding) ? Math.max(0, source.padding) : 0.1 )), { values } ); scale.range([0, bandwidth]); const groupBandwidth = scale.bandwidth?.(); if (groupBandwidth === void 0) { throw new TypeError("A grouped bar layout requires a D3 band scale"); } return { bandwidth: groupBandwidth, map(value) { if (value === null) { throw new TypeError( "A grouped bar requires an explicit z channel or a discrete color channel" ); } const position = scale(value); if (position === void 0 || !Number.isFinite(position)) { throw new TypeError( `Bar group value "${String(value)}" is outside the group layout scale domain` ); } return position; } }; } function hasDuplicateValues(values) { const seen = /* @__PURE__ */ new Set(); for (const value of values) { if (!isChartValue(value)) continue; const identity = valueKey(value); if (seen.has(identity)) return true; seen.add(identity); } return false; } function inferBandwidth(scale, values, span, count) { const positions = [ ...new Set( values.filter(isChartValue).map(scale.map).filter((value) => Number.isFinite(value)) ) ].sort((a, b) => a - b); let minimum = Infinity; for (let index = 1; index < positions.length; index += 1) { minimum = Math.min(minimum, positions[index] - positions[index - 1]); } return Number.isFinite(minimum) ? minimum * 0.8 : Math.min(48, span / Math.max(2, count + 1) * 0.8); } function numericChannelValues(data, channel, fallback) { return typeof channel === "number" ? data.map(() => channel) : channelValues(data, channel, fallback); } export { barX, barY };