UNPKG

react-native-chart-kit

Version:

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

368 lines (367 loc) 15.8 kB
import { useMemo } from "react"; import { buildLineSeriesGeometry, calculateAutoPadding, createLinearScale, solveChartBoxes } from "../../../core/index"; import { resolveCartesianChartThemeConfig } from "../../theme"; import { getLineChartAreaFillConfig, getLineChartCrosshairConfig, getLineChartDotConfig, getLineChartStrokeStyle, getLineChartTooltipConfig, resolveLineChartDecimationConfig } from "./options"; import { buildLineChartSeriesStyleMap } from "./seriesStyles"; import { resolveLineChartYAxisModel } from "./yAxisModel"; import { getSelectedLineSeries } from "./selection"; import { getLineChartTooltipModel } from "./tooltip"; import { buildLegendLayout, getLegendConfig, getLegendX, getLegendY } from "./legend"; import { buildLineChartReferenceBandModels, buildLineChartReferenceLineModels } from "./references"; import { getFontFamilyProps, measureLineChartText } from "./text"; import { defaultLabelRotation, getMaxSize, resolveXLabelLayout, xLabelBaselineOffset } from "./xLabels"; import { buildXLabelCandidates } from "./xLabelCandidates"; import { defaultFormatXLabel, defaultFormatYLabel, getSeriesColor } from "./utils"; import { normalizeLineChartSelectedIndex } from "./interaction"; import { useSeriesInput } from "./seriesInput"; import { buildLineChartXScale } from "./xScale"; const defaultYDomain = { includeZero: true, nice: true }; export const useChartModel = ({ data, xKey, yKey, yKeys, series, width, height, theme, preset, decimation = "auto", curve = "linear", connectNulls = false, area = false, areaFill, showDots = true, dots, selectedIndex, activeDot, crosshair, tooltip, referenceBands, referenceLines, showHorizontalGridLines = false, showVerticalGridLines = false, legend, labelStrategy = "auto", labelRotation = defaultLabelRotation, labelMinGap = 8, edgeLabelPolicy = "shift", yDomain = defaultYDomain, yAxisLabelWidth, formatXLabel = defaultFormatXLabel, formatYLabel = defaultFormatYLabel, chartKitTheme, dataIndexOffset = 0, selectionPointer, stableYAxisData }) => { const seriesInput = useSeriesInput(yKey, yKeys, series); return useMemo(() => { 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 axisTextOptions = { fontSize: resolvedTheme.typography.axisLabelSize, ...getFontFamilyProps(resolvedTheme.typography.fontFamily) }; const { normalized, yDomainResolved, yLabelSizes, yTicks } = resolveLineChartYAxisModel({ data, formatYLabel, measureText: measureLineChartText, series: seriesInput, stableData: stableYAxisData, textOptions: axisTextOptions, xKey, yAxisLabelWidth, yDomain }); const legendConfig = getLegendConfig(legend, normalized.series.length, resolvedTheme); const xValues = normalized.series[0]?.points.map((point) => point.x) ?? []; const xLabelTexts = xValues.map((value, index) => formatXLabel(value, index + dataIndexOffset)); const xLabelSizes = xLabelTexts.map((text) => measureLineChartText(text, axisTextOptions)); const styleByKey = buildLineChartSeriesStyleMap({ areaFill, dots, resolvedTheme, seriesInput, showDots }); const legendLayout = legendConfig.visible ? buildLegendLayout({ config: legendConfig, items: normalized.series.map((item) => ({ id: item.key, label: item.label })), maxWidth: width, measureText: measureLineChartText }) : undefined; const baseAutoPaddingOptions = { base: { top: 16, right: 18, bottom: 12, left: 10 }, leftLabels: yLabelSizes, gap: 8 }; const withLegendPaddingOptions = (bottomLabelHeight) => legendLayout ? { ...baseAutoPaddingOptions, bottomLabels: bottomLabelHeight > 0 ? [{ width: 1, height: bottomLabelHeight }] : [], legend: { width: Math.min(width - 32, legendLayout.width), height: legendLayout.height, position: legendLayout.position } } : { ...baseAutoPaddingOptions, bottomLabels: bottomLabelHeight > 0 ? [{ width: 1, height: bottomLabelHeight }] : [] }; const initialPadding = calculateAutoPadding(withLegendPaddingOptions(getMaxSize(xLabelSizes).height)); const initialBoxes = solveChartBoxes({ width, height }, initialPadding); const initialXScale = buildLineChartXScale({ boxes: initialBoxes, xValues }); const initialXLabelLayout = resolveXLabelLayout({ candidates: buildXLabelCandidates({ dataIndexOffset, labelStrategy, series: normalized.series[0], xLabelSizes, xLabelTexts, xScale: initialXScale, xValues }), plotWidth: initialBoxes.plot.width, chartWidth: width, strategy: labelStrategy, rotation: labelRotation, edgeLabelPolicy, minGap: labelMinGap, baseY: initialBoxes.plot.y + initialBoxes.plot.height + xLabelBaselineOffset }); const padding = calculateAutoPadding(withLegendPaddingOptions(initialXLabelLayout.height)); const boxes = solveChartBoxes({ width, height }, padding); const yScale = createLinearScale({ domain: yDomainResolved, range: [boxes.plot.y + boxes.plot.height, boxes.plot.y] }); const referenceBandModels = buildLineChartReferenceBandModels({ bands: referenceBands, plot: boxes.plot, theme: resolvedTheme, yScale }); const xScale = buildLineChartXScale({ boxes, xValues }); const pathDecimation = resolveLineChartDecimationConfig({ decimation, plotWidth: boxes.plot.width }); const baselineValue = yDomainResolved[0] < 0 && yDomainResolved[1] > 0 ? 0 : yDomainResolved[0]; const baselineY = yScale.scale(baselineValue); const geometries = normalized.series.map((item, index) => { const style = styleByKey.get(item.key); const wantsArea = style?.area ?? area; const color = item.color ?? style?.color ?? getSeriesColor(resolvedTheme, index); return { style: { strokeWidth: style?.strokeWidth ?? 3, strokeStyle: style?.strokeStyle ?? getLineChartStrokeStyle(), dot: style?.dot ?? getLineChartDotConfig({ dots, seriesDot: undefined, showDots }), color, areaFill: style?.areaFill ?? getLineChartAreaFillConfig({ areaFill, seriesColor: color }), threshold: style?.threshold }, geometry: buildLineSeriesGeometry({ series: item, xScale, yScale: (value) => yScale.scale(value), curve: style?.curve ?? curve, connectNulls, dataIndexOffset, pathDecimation, ...(wantsArea ? { areaBaselineY: baselineY } : {}) }) }; }); const referenceLineModels = buildLineChartReferenceLineModels({ geometries: geometries.map(({ geometry }) => geometry), lines: referenceLines, plot: boxes.plot, theme: resolvedTheme, yScale }); const interactionPoints = geometries[0]?.geometry.points.flatMap((point) => { if (!Number.isFinite(point.x)) { return []; } const dataIndex = point.dataIndex; const xLabel = xLabelTexts[dataIndex - dataIndexOffset] ?? String(point.xValue); const interactionPoint = { dataIndex, x: point.x, xValue: point.xValue, xLabel }; return point.raw !== undefined ? [{ ...interactionPoint, raw: point.raw }] : [interactionPoint]; }) ?? []; const xLabelLayout = resolveXLabelLayout({ candidates: buildXLabelCandidates({ dataIndexOffset, labelStrategy, series: normalized.series[0], xLabelSizes, xLabelTexts, xScale, xValues }), plotWidth: boxes.plot.width, chartWidth: width, strategy: labelStrategy, rotation: labelRotation, edgeLabelPolicy, minGap: labelMinGap, baseY: boxes.plot.y + boxes.plot.height + xLabelBaselineOffset }); const crosshairConfig = getLineChartCrosshairConfig({ crosshair, themeAxisColor: resolvedTheme.axis }); const tooltipConfig = getLineChartTooltipConfig({ tooltip, themeFontFamily: resolvedTheme.typography.fontFamily, themeTooltip: resolvedTheme.tooltip }); const roundedSelectedIndex = normalizeLineChartSelectedIndex(selectedIndex); const selectedDataIndex = roundedSelectedIndex !== undefined && roundedSelectedIndex >= dataIndexOffset && roundedSelectedIndex < dataIndexOffset + xValues.length ? roundedSelectedIndex : undefined; const selectedSeries = getSelectedLineSeries({ activeDot, formatYLabel, geometries, selectedDataIndex }); const selectionBase = selectedDataIndex !== undefined && selectedSeries.length > 0 ? { index: selectedDataIndex, x: selectedSeries[0].point.x, y: Math.min(...selectedSeries.map((item) => item.point.y)), xLabel: xLabelTexts[selectedDataIndex - dataIndexOffset] ?? String(selectedDataIndex), series: selectedSeries } : undefined; const selectionModel = selectionBase ? { ...selectionBase, tooltip: getLineChartTooltipModel({ chartHeight: height, chartWidth: width, config: tooltipConfig, measureText: measureLineChartText, plotX: boxes.plot.x, plotY: boxes.plot.y, selection: selectionPointer?.index === selectionBase.index ? { ...selectionBase, pointer: selectionPointer } : selectionBase, theme: resolvedTheme }) } : undefined; const legendOrigin = legendLayout && legendLayout.items.length > 0 ? { x: getLegendX({ align: legendConfig.align, boxes, legendWidth: legendLayout.width, width }), y: getLegendY({ boxes, xLabelHeight: xLabelLayout.height, xLabelLineHeight: getMaxSize(xLabelSizes).height, legendHeight: legendLayout.height, position: legendConfig.position }) } : undefined; const legendModel = legendLayout && legendOrigin ? { config: legendConfig, renderProps: { x: legendOrigin.x, y: legendOrigin.y, width: legendLayout.width, height: legendLayout.height, position: legendConfig.position, align: legendConfig.align, theme: resolvedTheme, items: legendLayout.items.map((item, index) => { const style = styleByKey.get(item.id); return { index, key: item.id, label: item.label, color: style?.color ?? getSeriesColor(resolvedTheme, index), x: legendOrigin.x + item.x, y: legendOrigin.y + item.y, width: item.width, height: item.height, contentX: legendOrigin.x + item.contentX, contentY: legendOrigin.y + item.contentY, contentWidth: item.contentWidth, contentHeight: item.contentHeight, markerSize: item.markerSize ?? legendConfig.markerSize, marker: legendConfig.marker, fontSize: legendConfig.fontSize, ...getFontFamilyProps(legendConfig.fontFamily), labelColor: legendConfig.labelColor, labelGap: legendConfig.labelGap, paddingHorizontal: legendConfig.itemPaddingHorizontal, paddingVertical: legendConfig.itemPaddingVertical, strokeDasharray: style?.strokeStyle.strokeDasharray, strokeLinecap: style?.strokeStyle.strokeLinecap ?? "round", strokeOpacity: style?.strokeStyle.strokeOpacity ?? 1, strokeWidth: style?.strokeWidth ?? 3 }; }) } } : undefined; return { boxes, geometries, interactionPoints, legendModel, referenceBandModels, referenceLineModels, resolvedTheme, showHorizontalGridLines, showVerticalGridLines, crosshairConfig, selectionModel, xLabelLayout, yScale, yTicks, formatYLabel }; }, [ area, areaFill, activeDot, connectNulls, crosshair, curve, data, decimation, dataIndexOffset, dots, formatXLabel, formatYLabel, height, edgeLabelPolicy, labelMinGap, labelRotation, labelStrategy, legend, referenceBands, referenceLines, selectedIndex, selectionPointer, seriesInput, showDots, showHorizontalGridLines, showVerticalGridLines, stableYAxisData, theme, preset, chartKitTheme, tooltip, width, xKey, yAxisLabelWidth, yDomain ]); };