@tanstack/charts
Version:
<div align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/api/readme/charts.png?theme=dark" /> <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/
558 lines (557 loc) • 21.1 kB
JavaScript
import { scaleBand } from "d3-scale";
import {
channelValues,
createMark,
inferredKeyValues,
isChartKey,
isChartValue,
isFiniteNumber,
markStates,
visualValue
} from "./mark.js";
import { resolveScaleInput } from "./scale-input.js";
import {
markStatesMayUseCornerRadii,
resolveSceneRectRadius
} from "./rect-radius-state-internal.js";
import {
sceneMotionNode
} from "./scene-motion-internal.js";
import { valueKey } from "./scales.js";
import { stackOuterEnds } from "./stack-ends-internal.js";
import { stackValues } from "./stack-internal.js";
function barY(source, options = {}) {
const data = Array.isArray(source) ? source : Array.from(source);
const xScale = options.xScale ?? "x";
const yScale = options.yScale ?? "y";
const preferCornerRadii = typeof options.radius === "function" || markStatesMayUseCornerRadii(options.states);
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";
validateEndRadius(options.radius, explicitExtent, grouped, "y");
const stackLayout = options.layout?.type === "stack" ? options.layout : void 0;
const endRadius = isBarEndRadius(options.radius) ? options.radius : void 0;
const divergingStack = stackLayout?.offset === void 0 || stackLayout.offset === "diverging";
const stacked = !explicitExtent && !grouped ? stackValues(xValues, rawYValues, seriesValues, stackLayout, "index") : void 0;
const outerEnds = stacked && endRadius && (endRadius.stack ?? "outer") === "outer" ? stackOuterEnds(
xValues,
rawYValues,
seriesValues,
stacked.starts,
stacked.ends,
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: xScale, values: xValues.filter(isChartValue) },
y: {
scale: yScale,
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[xScale].bandwidth || inferBandwidth(scales[xScale], 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[xScale].map(xValue);
const baselinePosition = scales[yScale].map(y1Value);
const valuePosition = scales[yScale].map(y2Value);
const valueEnd = stacked && divergingStack && yValue < 0 ? "start" : "end";
const semanticValuePosition = valueEnd === "start" ? baselinePosition : valuePosition;
const semanticBaselinePosition = valueEnd === "start" ? valuePosition : baselinePosition;
const outerEnd = outerEnds?.[datumIndex];
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 resolvedRadius = resolveBarRadius(
options.radius,
datum,
datumIndex,
data,
"y",
semanticValuePosition,
semanticBaselinePosition,
outerEnds !== void 0,
outerEnd,
baselinePosition,
valuePosition,
preferCornerRadii
);
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: semanticValuePosition,
color: fill
};
const node = {
kind: "rect",
key,
x,
y,
width,
height,
...resolvedRadius,
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
},
[sceneMotionNode]: {
bar: { baseline: semanticBaselinePosition }
}
};
nodes.push(node);
});
return {
nodes: [
{
kind: "group",
key: id,
className: "ts-chart__bar ts-chart__bar-y",
ariaHidden: true,
children: nodes
}
]
};
}
};
},
options.motion,
options.renderer
);
}
function barX(source, options = {}) {
const data = Array.isArray(source) ? source : Array.from(source);
const xScale = options.xScale ?? "x";
const yScale = options.yScale ?? "y";
const preferCornerRadii = typeof options.radius === "function" || markStatesMayUseCornerRadii(options.states);
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";
validateEndRadius(options.radius, explicitExtent, grouped, "x");
const stackLayout = options.layout?.type === "stack" ? options.layout : void 0;
const endRadius = isBarEndRadius(options.radius) ? options.radius : void 0;
const divergingStack = stackLayout?.offset === void 0 || stackLayout.offset === "diverging";
const stacked = !explicitExtent && !grouped ? stackValues(yValues, rawXValues, seriesValues, stackLayout, "index") : void 0;
const outerEnds = stacked && endRadius && (endRadius.stack ?? "outer") === "outer" ? stackOuterEnds(
yValues,
rawXValues,
seriesValues,
stacked.starts,
stacked.ends,
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: xScale,
values: [
...x2Values.filter(isFiniteNumber),
...x1Values.filter(isFiniteNumber)
],
includeZero: options.x1 === void 0
},
y: { scale: yScale, values: yValues.filter(isChartValue) },
color: {
scale: "color",
values: colorValues.filter(isChartKey)
}
},
render: ({ scales, chart, color: resolveColor }) => {
const totalBandwidth = scales[yScale].bandwidth || inferBandwidth(scales[yScale], 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[xScale].map(x1Value);
const valuePosition = scales[xScale].map(x2Value);
const valueEnd = stacked && divergingStack && xValue < 0 ? "start" : "end";
const semanticValuePosition = valueEnd === "start" ? baselinePosition : valuePosition;
const semanticBaselinePosition = valueEnd === "start" ? valuePosition : baselinePosition;
const outerEnd = outerEnds?.[datumIndex];
const center = scales[yScale].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 resolvedRadius = resolveBarRadius(
options.radius,
datum,
datumIndex,
data,
"x",
semanticValuePosition,
semanticBaselinePosition,
outerEnds !== void 0,
outerEnd,
baselinePosition,
valuePosition,
preferCornerRadii
);
const point = {
key,
markId: id,
group,
groupLabel: group == null ? id : String(group),
datum,
datumIndex,
xValue,
yValue,
x1Value,
x2Value,
xInterval: "difference",
x: semanticValuePosition,
y: center - totalBandwidth / 2 + groupOffset + groupBandwidth / 2,
color: fill
};
const node = {
kind: "rect",
key,
x,
y,
width,
height,
...resolvedRadius,
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
},
[sceneMotionNode]: {
bar: { baseline: semanticBaselinePosition }
}
};
nodes.push(node);
});
return {
nodes: [
{
kind: "group",
key: id,
className: "ts-chart__bar ts-chart__bar-x",
ariaHidden: true,
children: nodes
}
]
};
}
};
},
options.motion,
options.renderer
);
}
function validateEndRadius(radius, explicitExtent, grouped, valueAxis) {
if (!isBarEndRadius(radius) || radius.stack !== "outer") return;
if (explicitExtent) {
throw new TypeError(
`Bar radius stack "outer" requires an implicit ${valueAxis} extent`
);
}
if (grouped) {
throw new TypeError(
'Bar radius stack "outer" cannot be used with a group layout'
);
}
}
function resolveBarRadius(radius, datum, index, data, valueAxis, valuePosition, baselinePosition, stackOuter, outerEnd, stackStartPosition, stackEndPosition, preferCornerRadii) {
if (radius === void 0) {
return resolveSceneRectRadius(void 0, preferCornerRadii);
}
if (isBarEndRadius(radius)) {
const endRadius = visualValue(radius.end, datum, index, data, 0);
if (valuePosition === baselinePosition) {
return { cornerRadii: [0, 0, 0, 0] };
}
if (stackOuter) {
if (!outerEnd) return { cornerRadii: [0, 0, 0, 0] };
if (outerEnd.start && outerEnd.end) {
return { cornerRadii: [endRadius, endRadius, endRadius, endRadius] };
}
const roundStart = outerEnd.start;
return {
cornerRadii: endCornerRadii(
endRadius,
valueAxis,
roundStart ? stackStartPosition : stackEndPosition,
roundStart ? stackEndPosition : stackStartPosition
)
};
}
return {
cornerRadii: endCornerRadii(
endRadius,
valueAxis,
valuePosition,
baselinePosition
)
};
}
const resolved = visualValue(radius, datum, index, data, 0);
return resolveSceneRectRadius(resolved, preferCornerRadii);
}
function isBarEndRadius(radius) {
return radius !== void 0 && radius !== null && typeof radius === "object" && !Array.isArray(radius);
}
function endCornerRadii(radius, valueAxis, valuePosition, baselinePosition) {
if (valueAxis === "y") {
return valuePosition <= baselinePosition ? [radius, radius, 0, 0] : [0, 0, radius, radius];
}
return valuePosition <= baselinePosition ? [radius, 0, 0, radius] : [0, radius, radius, 0];
}
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
};