react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
368 lines (367 loc) • 15.8 kB
JavaScript
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
]);
};