react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
203 lines (202 loc) • 7.33 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 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 buildRect = ({ baselineX, dataIndex, height, raw, scaledValue, series, seriesIndex, value, widthValue, xScale, xValue, y }) => {
const valueX = xScale(widthValue);
if (!isFinitePosition(valueX)) {
return undefined;
}
return {
key: `${series.key}-${dataIndex}`,
seriesKey: series.key,
seriesLabel: series.label,
seriesIndex,
dataIndex,
xValue,
value,
scaledValue,
x: Math.min(valueX, baselineX),
y,
width: Math.abs(valueX - baselineX),
height,
baselineX,
baselineY: y + height / 2,
defined: true,
raw
};
};
const buildGroupedHorizontalBars = ({ baselineX, barGapRatio, barWidthRatio, dataIndexOffset, series, xScale, yBand }) => {
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 = yBand(referencePoint.x, rowIndex + dataIndexOffset, referencePoint.raw);
if (!band || !Number.isFinite(band.y) || !Number.isFinite(band.height)) {
continue;
}
const groupHeight = band.height * barWidthRatio;
const groupY = band.y + (band.height - groupHeight) / 2;
const totalGapHeight = groupHeight * barGapRatio * Math.max(0, seriesCount - 1);
const barHeight = Math.max(0, (groupHeight - totalGapHeight) / seriesCount);
const gapHeight = seriesCount > 1 ? groupHeight * 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({
baselineX,
dataIndex: rowIndex + dataIndexOffset,
height: barHeight,
raw: point.raw,
scaledValue: point.value,
series: item,
seriesIndex,
value: point.value,
widthValue: point.value,
xScale,
xValue: point.x,
y: groupY + seriesIndex * (barHeight + gapHeight)
});
if (rect) {
bars.push(rect);
}
});
}
return bars;
};
const buildStackedHorizontalBars = ({ baselineX, barWidthRatio, dataIndexOffset, mode, series, xScale, yBand }) => {
const rowCount = getSeriesRowCount(series);
const bars = [];
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const referencePoint = series[0]?.points[rowIndex];
if (!referencePoint) {
continue;
}
const band = yBand(referencePoint.x, rowIndex + dataIndexOffset, referencePoint.raw);
if (!band || !Number.isFinite(band.y) || !Number.isFinite(band.height)) {
continue;
}
let positiveStart = 0;
let negativeStart = 0;
const barHeight = band.height * barWidthRatio;
const y = band.y + (band.height - barHeight) / 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 stackStartX = xScale(stackStart);
const stackEndX = xScale(stackEnd);
if (!isFinitePosition(stackStartX) || !isFinitePosition(stackEndX)) {
return;
}
bars.push({
key: `${item.key}-${rowIndex + dataIndexOffset}`,
seriesKey: item.key,
seriesLabel: item.label,
seriesIndex,
dataIndex: rowIndex + dataIndexOffset,
xValue: point.x,
value: point.value,
scaledValue,
x: Math.min(stackStartX, stackEndX),
y,
width: Math.abs(stackEndX - stackStartX),
height: barHeight,
baselineX,
baselineY: y + barHeight / 2,
defined: true,
raw: point.raw,
stackStart,
stackEnd
});
if (scaledValue >= 0) {
positiveStart = stackEnd;
}
else {
negativeStart = stackEnd;
}
});
}
return bars;
};
export const buildHorizontalBarGeometry = ({ series, yBand, xScale, mode = "grouped", baselineValue = 0, barWidthRatio: rawBarWidthRatio, barGapRatio: rawBarGapRatio, dataIndexOffset = 0 }) => {
const baselineX = xScale(baselineValue);
if (!isFinitePosition(baselineX) || series.length === 0) {
return { mode, bars: [] };
}
const barWidthRatio = clampRatio(rawBarWidthRatio, defaultBarWidthRatio);
const barGapRatio = clampRatio(rawBarGapRatio, defaultBarGapRatio);
const bars = mode === "grouped"
? buildGroupedHorizontalBars({
baselineX,
barGapRatio,
barWidthRatio,
dataIndexOffset,
series,
xScale,
yBand
})
: buildStackedHorizontalBars({
baselineX,
barWidthRatio,
dataIndexOffset,
mode,
series,
xScale,
yBand
});
return {
mode,
bars
};
};