react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
202 lines (201 loc) • 7.21 kB
JavaScript
const defaultBarWidthRatio = 0.72;
const defaultBarGapRatio = 0.12;
const clampRatio = (value, fallback) => typeof value === "number" && Number.isFinite(value)
? Math.max(0, Math.min(1, value))
: fallback;
const isFinitePosition = (value) => typeof value === "number" && Number.isFinite(value);
const buildRect = ({ baselineY, dataIndex, heightValue, raw, scaledValue, series, seriesIndex, value, width, x, xValue, yScale }) => {
const valueY = yScale(heightValue);
if (!isFinitePosition(valueY)) {
return undefined;
}
return {
key: `${series.key}-${dataIndex}`,
seriesKey: series.key,
seriesLabel: series.label,
seriesIndex,
dataIndex,
xValue,
value,
scaledValue,
x,
y: Math.min(valueY, baselineY),
width,
height: Math.abs(baselineY - valueY),
baselineY,
defined: true,
raw
};
};
const getSeriesPoint = (series, rowIndex) => series.points[rowIndex];
const getSeriesRowCount = (series) => Math.max(0, ...series.map((item) => item.points.length));
const getStackTotals = (series, rowIndex) => {
let positive = 0;
let negative = 0;
series.forEach((item) => {
const value = getSeriesPoint(item, rowIndex)?.value;
if (typeof value !== "number" || !Number.isFinite(value)) {
return;
}
if (value >= 0) {
positive += value;
}
else {
negative += Math.abs(value);
}
});
return { negative, positive };
};
const getStackedScaledValue = ({ mode, rowIndex, series, value }) => {
if (mode !== "stacked100") {
return value;
}
const totals = getStackTotals(series, rowIndex);
if (value >= 0) {
return totals.positive > 0 ? (value / totals.positive) * 100 : 0;
}
return totals.negative > 0 ? (value / totals.negative) * 100 : 0;
};
const buildGroupedBars = ({ baselineY, barGapRatio, barWidthRatio, dataIndexOffset, series, xBand, yScale }) => {
const rowCount = getSeriesRowCount(series);
const bars = [];
const seriesCount = Math.max(1, series.length);
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const referencePoint = series[0]?.points[rowIndex];
if (!referencePoint) {
continue;
}
const band = xBand(referencePoint.x, rowIndex + dataIndexOffset, referencePoint.raw);
if (!band || !Number.isFinite(band.x) || !Number.isFinite(band.width)) {
continue;
}
const groupWidth = band.width * barWidthRatio;
const groupX = band.x + (band.width - groupWidth) / 2;
const totalGapWidth = groupWidth * barGapRatio * Math.max(0, seriesCount - 1);
const barWidth = Math.max(0, (groupWidth - totalGapWidth) / seriesCount);
const gapWidth = seriesCount > 1 ? groupWidth * barGapRatio : 0;
series.forEach((item, seriesIndex) => {
const point = getSeriesPoint(item, rowIndex);
if (!point ||
typeof point.value !== "number" ||
!Number.isFinite(point.value)) {
return;
}
const rect = buildRect({
baselineY,
dataIndex: rowIndex + dataIndexOffset,
heightValue: point.value,
raw: point.raw,
scaledValue: point.value,
series: item,
seriesIndex,
value: point.value,
width: barWidth,
x: groupX + seriesIndex * (barWidth + gapWidth),
xValue: point.x,
yScale
});
if (rect) {
bars.push(rect);
}
});
}
return bars;
};
const buildStackedBars = ({ baselineY, barWidthRatio, dataIndexOffset, mode, series, xBand, yScale }) => {
const rowCount = getSeriesRowCount(series);
const bars = [];
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const referencePoint = series[0]?.points[rowIndex];
if (!referencePoint) {
continue;
}
const band = xBand(referencePoint.x, rowIndex + dataIndexOffset, referencePoint.raw);
if (!band || !Number.isFinite(band.x) || !Number.isFinite(band.width)) {
continue;
}
let positiveStart = 0;
let negativeStart = 0;
const barWidth = band.width * barWidthRatio;
const x = band.x + (band.width - barWidth) / 2;
series.forEach((item, seriesIndex) => {
const point = getSeriesPoint(item, rowIndex);
if (!point ||
typeof point.value !== "number" ||
!Number.isFinite(point.value)) {
return;
}
const scaledValue = getStackedScaledValue({
mode,
rowIndex,
series,
value: point.value
});
const stackStart = scaledValue >= 0 ? positiveStart : negativeStart;
const stackEnd = stackStart + scaledValue;
const stackStartY = yScale(stackStart);
const stackEndY = yScale(stackEnd);
if (!isFinitePosition(stackStartY) || !isFinitePosition(stackEndY)) {
return;
}
const rect = {
key: `${item.key}-${rowIndex + dataIndexOffset}`,
seriesKey: item.key,
seriesLabel: item.label,
seriesIndex,
dataIndex: rowIndex + dataIndexOffset,
xValue: point.x,
value: point.value,
scaledValue,
x,
y: Math.min(stackStartY, stackEndY),
width: barWidth,
height: Math.abs(stackStartY - stackEndY),
baselineY,
defined: true,
raw: point.raw,
stackStart,
stackEnd
};
bars.push(rect);
if (scaledValue >= 0) {
positiveStart = stackEnd;
}
else {
negativeStart = stackEnd;
}
});
}
return bars;
};
export const buildBarGeometry = ({ series, xBand, yScale, mode = "grouped", baselineValue = 0, barWidthRatio: rawBarWidthRatio, barGapRatio: rawBarGapRatio, dataIndexOffset = 0 }) => {
const baselineY = yScale(baselineValue);
if (!isFinitePosition(baselineY) || series.length === 0) {
return { mode, bars: [] };
}
const barWidthRatio = clampRatio(rawBarWidthRatio, defaultBarWidthRatio);
const barGapRatio = clampRatio(rawBarGapRatio, defaultBarGapRatio);
const bars = mode === "grouped"
? buildGroupedBars({
baselineY,
barGapRatio,
barWidthRatio,
dataIndexOffset,
series,
xBand,
yScale
})
: buildStackedBars({
baselineY,
barWidthRatio,
dataIndexOffset,
mode,
series,
xBand,
yScale
});
return {
mode,
bars
};
};