UNPKG

react-native-chart-kit

Version:

Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.

327 lines (326 loc) 12.8 kB
import { buildBarGeometry, buildHorizontalBarGeometry, calculateAutoPadding, createBandScale, createLinearScale, generateLinearTicks, normalizeCartesianData, resolveNumericDomain, solveChartBoxes } from "../../../core/index"; import { resolveCartesianChartThemeConfig } from "../../theme/presets"; import { defaultFormatBarChartXLabel, defaultFormatBarChartYLabel, getBarChartFontTextOptions, getBarChartSeriesColor, getBarChartXKey, getMaxBarChartTextSize, getVisibleBarChartXLabelInterval, getVisibleBarChartYLabelInterval, labelBaselineOffset, measureBarChartText } from "./modelUtils"; const defaultYDomain = { includeZero: true, nice: true }; const resolveSeriesInput = (yKey, yKeys, series) => { if (series && series.length > 0) { return series; } if (yKeys && yKeys.length > 0) { return yKeys.map((key) => ({ yKey: key })); } return yKey ? [{ yKey }] : []; }; const getDomainValues = ({ mode, series, yDomain }) => { if (mode === "stacked100" && yDomain === defaultYDomain) { return [0, 100]; } if (mode === "grouped") { return series.flatMap((item) => item.points.flatMap((point) => typeof point.value === "number" ? [point.value] : [])); } const rowCount = Math.max(0, ...series.map((item) => item.points.length)); const values = []; for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { let positive = 0; let negative = 0; series.forEach((item) => { const value = item.points[rowIndex]?.value; if (typeof value !== "number") { return; } if (value >= 0) { positive += value; } else { negative += value; } }); values.push(positive, negative); } return values; }; export const buildBarChartModel = ({ data, xKey, yKey, yKeys, series, width, height, theme, preset, mode = "grouped", orientation = "vertical", yDomain = defaultYDomain, barWidthRatio = 0.72, barGapRatio = 0.12, showValuesOnTopOfBars = false, showHorizontalGridLines = true, showXAxisLabels = true, showYAxisLabels = true, yTickCount = 5, legend, labelStrategy = "auto", formatXLabel = defaultFormatBarChartXLabel, formatYLabel = defaultFormatBarChartYLabel, chartKitTheme }) => { const resolvedTheme = resolveCartesianChartThemeConfig({ mode: typeof theme === "string" && theme !== "system" ? theme : chartKitTheme.mode, preset: preset ?? chartKitTheme.preset, presets: chartKitTheme.presets, theme: typeof theme === "object" ? theme : chartKitTheme.theme }); const seriesInput = resolveSeriesInput(yKey, yKeys, series); const legendVisible = legend ?? seriesInput.length > 1; const normalized = normalizeCartesianData({ data, xKey, series: seriesInput }); const xValues = normalized.series[0]?.points.map((point) => point.x) ?? []; const xDomain = xValues.map(getBarChartXKey); const xLabelTexts = xValues.map((value, index) => formatXLabel(value, index)); const textOptions = getBarChartFontTextOptions(resolvedTheme); const xLabelSizes = xLabelTexts.map((text) => measureBarChartText(text, textOptions)); const isHorizontal = orientation === "horizontal"; const domainValues = getDomainValues({ mode, series: normalized.series, yDomain }); const resolvedYDomain = mode === "stacked100" && yDomain === defaultYDomain ? [0, 100] : resolveNumericDomain(domainValues, yDomain); const yTicks = generateLinearTicks({ domain: resolvedYDomain, count: Math.max(2, Math.round(yTickCount)) }); const yLabelSizes = yTicks.map((tick) => measureBarChartText(formatYLabel(tick), textOptions)); const legendFontSize = resolvedTheme.typography.legendLabelSize; const legendMarkerSize = 8; const legendGap = 14; const legendItemsRaw = seriesInput.map((item, index) => ({ key: item.key ?? item.yKey, label: item.label ?? item.key ?? item.yKey, color: item.color ?? getBarChartSeriesColor(resolvedTheme, index), width: legendMarkerSize + 6 + measureBarChartText(item.label ?? item.key ?? item.yKey, { fontSize: legendFontSize }).width })); const legendHeight = legendVisible && legendItemsRaw.length > 0 ? 18 : 0; const autoPaddingOptions = { base: { top: 18, right: 14, bottom: 12, left: 10 }, leftLabels: isHorizontal ? xLabelSizes : yLabelSizes, bottomLabels: isHorizontal ? yLabelSizes : xLabelSizes.length > 0 ? [getMaxBarChartTextSize(xLabelSizes)] : [], gap: 8 }; const basePadding = calculateAutoPadding(legendHeight > 0 ? { ...autoPaddingOptions, legend: { height: legendHeight, position: "bottom", width: width - 24 } } : autoPaddingOptions); const boxes = solveChartBoxes({ width, height }, basePadding); const yScale = createLinearScale({ domain: resolvedYDomain, range: [boxes.plot.y + boxes.plot.height, boxes.plot.y] }); const xScale = createBandScale({ domain: xDomain, range: [boxes.plot.x, boxes.plot.x + boxes.plot.width], paddingInner: 0.12, paddingOuter: 0.08 }); const horizontalXScale = createLinearScale({ domain: resolvedYDomain, range: [boxes.plot.x, boxes.plot.x + boxes.plot.width] }); const horizontalYScale = createBandScale({ domain: xDomain, range: [boxes.plot.y, boxes.plot.y + boxes.plot.height], paddingInner: 0.12, paddingOuter: 0.08 }); const barGeometry = buildBarGeometry({ series: normalized.series, mode, xBand: (value) => { const key = getBarChartXKey(value); const x = xScale.scale(key); return x !== undefined ? { x, width: xScale.bandwidth } : undefined; }, yScale: (value) => yScale.scale(value), barWidthRatio, barGapRatio }); const horizontalBarGeometry = buildHorizontalBarGeometry({ series: normalized.series, mode, yBand: (value) => { const key = getBarChartXKey(value); const y = horizontalYScale.scale(key); return y !== undefined ? { y, height: horizontalYScale.bandwidth } : undefined; }, xScale: (value) => horizontalXScale.scale(value), barWidthRatio, barGapRatio }); const verticalLabelInterval = getVisibleBarChartXLabelInterval({ labelStrategy, labelSizes: xLabelSizes, plotWidth: boxes.plot.width }); const horizontalLabelInterval = getVisibleBarChartYLabelInterval({ labelStrategy, labelSizes: xLabelSizes, plotHeight: boxes.plot.height }); const verticalXLabels = xLabelTexts.flatMap((text, index) => { if (index % verticalLabelInterval !== 0) { return []; } const key = xDomain[index]; const bandX = key !== undefined ? xScale.scale(key) : undefined; if (bandX === undefined) { return []; } return [ { index, text, x: bandX + xScale.bandwidth / 2, y: boxes.plot.y + boxes.plot.height + labelBaselineOffset, textAnchor: "middle" } ]; }); const verticalYLabels = yTicks.map((tick) => ({ key: `tick-${tick}`, text: formatYLabel(tick), x: boxes.plot.x - 8, y: yScale.scale(tick) + resolvedTheme.typography.axisLabelSize / 2 - 2 })); const horizontalXLabels = yTicks.map((tick, index) => ({ index, text: formatYLabel(tick), x: horizontalXScale.scale(tick), y: boxes.plot.y + boxes.plot.height + labelBaselineOffset, textAnchor: "middle" })); const horizontalYLabels = xLabelTexts.flatMap((text, index) => { if (index % horizontalLabelInterval !== 0) { return []; } const key = xDomain[index]; const bandY = key !== undefined ? horizontalYScale.scale(key) : undefined; if (bandY === undefined) { return []; } return [ { key: `category-${index}`, text, x: boxes.plot.x - 8, y: bandY + horizontalYScale.bandwidth / 2 + resolvedTheme.typography.axisLabelSize / 2 - 2 } ]; }); const verticalBars = barGeometry.bars.map((bar) => ({ baselineY: bar.baselineY, color: normalized.series[bar.seriesIndex]?.color ?? seriesInput[bar.seriesIndex]?.color ?? getBarChartSeriesColor(resolvedTheme, bar.seriesIndex), dataIndex: bar.dataIndex, formattedValue: formatYLabel(bar.value), height: bar.height, key: bar.key, raw: bar.raw, seriesIndex: bar.seriesIndex, seriesKey: bar.seriesKey, seriesLabel: bar.seriesLabel, value: bar.value, width: bar.width, x: bar.x, xLabel: formatXLabel(bar.xValue, bar.dataIndex), xValue: bar.xValue, y: bar.y })); const horizontalBars = horizontalBarGeometry.bars.map((bar) => ({ baselineX: bar.baselineX, baselineY: bar.baselineY, color: normalized.series[bar.seriesIndex]?.color ?? seriesInput[bar.seriesIndex]?.color ?? getBarChartSeriesColor(resolvedTheme, bar.seriesIndex), dataIndex: bar.dataIndex, formattedValue: formatYLabel(bar.value), height: bar.height, key: bar.key, raw: bar.raw, seriesIndex: bar.seriesIndex, seriesKey: bar.seriesKey, seriesLabel: bar.seriesLabel, value: bar.value, width: bar.width, x: bar.x, xLabel: formatXLabel(bar.xValue, bar.dataIndex), xValue: bar.xValue, y: bar.y })); const bars = isHorizontal ? horizontalBars : verticalBars; const valueLabels = showValuesOnTopOfBars ? bars.map((bar) => isHorizontal ? { key: `value-${bar.key}`, text: formatYLabel(bar.value), x: bar.value >= 0 ? Math.min(boxes.plot.x + boxes.plot.width - 2, bar.x + bar.width + 5) : Math.max(boxes.plot.x + 2, bar.x - 5), y: bar.y + bar.height / 2 + resolvedTheme.typography.axisLabelSize / 2 - 2, color: resolvedTheme.mutedText, textAnchor: bar.value >= 0 ? "start" : "end" } : { key: `value-${bar.key}`, text: formatYLabel(bar.value), x: bar.x + bar.width / 2, y: bar.value >= 0 ? Math.max(boxes.plot.y + 10, bar.y - 5) : Math.min(boxes.plot.y + boxes.plot.height - 2, bar.y + bar.height + resolvedTheme.typography.axisLabelSize), color: resolvedTheme.mutedText }) : []; const legendWidth = legendItemsRaw.reduce((sum, item) => sum + item.width, 0) + legendGap * Math.max(0, legendItemsRaw.length - 1); const legendStartX = Math.max(8, (width - legendWidth) / 2); const legendY = height - 8; const legendItems = legendVisible ? legendItemsRaw.map((item, index) => { const x = legendStartX + legendItemsRaw .slice(0, index) .reduce((sum, previous) => sum + previous.width + legendGap, 0); return { key: item.key, label: item.label, color: item.color, markerX: x, markerY: legendY - legendMarkerSize + 1, labelX: x + legendMarkerSize + 6, labelY: legendY }; }) : []; return { bars, boxes, mode, orientation, resolvedTheme, legendItems, showHorizontalGridLines, showXAxisLabels, showYAxisLabels, valueLabels, xLabels: isHorizontal ? horizontalXLabels : verticalXLabels, yLabels: isHorizontal ? horizontalYLabels : verticalYLabels, yTicks }; };