UNPKG

react-native-chart-kit

Version:

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

160 lines (159 loc) 9.01 kB
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useCallback, useMemo, useRef } from "react"; import { StyleSheet, View } from "react-native"; import { useChartKitTheme } from "../../theme"; import { getLineChartRenderer as getContributionGraphRenderer } from "../line/renderer"; import { buildContributionGraphModel, getContributionGraphMonthLabel, getContributionGraphWeekdayLabel } from "./model"; import { getContributionGraphAccessibilitySummary } from "./accessibility"; import { buildContributionGraphDayPressEvent, getContributionGraphCellAtPoint, getContributionGraphCellKey, getContributionGraphInteractionConfig } from "./interaction"; export { getContributionGraphAccessibilitySummary, getContributionGraphDataTable } from "./accessibility"; const RendererLayer = ({ children }) => _jsx(_Fragment, { children: children }); const activeCellDefaultScale = 1.8; const getDateKey = (input) => { if (input === undefined) { return undefined; } if (typeof input === "string" && /^\d{4}-\d{2}-\d{2}$/.test(input)) { return input; } const date = input instanceof Date ? input : new Date(input); if (Number.isNaN(date.valueOf())) { return undefined; } const year = date.getFullYear(); const month = `${date.getMonth() + 1}`.padStart(2, "0"); const day = `${date.getDate()}`.padStart(2, "0"); return `${year}-${month}-${day}`; }; const normalizeActiveCellScale = (scale) => typeof scale === "number" && Number.isFinite(scale) && scale >= 1 ? scale : activeCellDefaultScale; const getActiveCellGeometry = ({ activeCell, cell }) => { const scale = normalizeActiveCellScale(activeCell?.scale); const size = cell.size * scale; const offset = (size - cell.size) / 2; return { rx: Math.max(3, cell.size * 0.42), size, x: cell.x - offset, y: cell.y - offset }; }; export const ContributionGraph = (props) => { const chartKitTheme = useChartKitTheme(); const interactionConfig = useMemo(() => getContributionGraphInteractionConfig(props.interaction), [props.interaction]); const lastSelectedCellKeyRef = useRef(undefined); const model = useMemo(() => buildContributionGraphModel({ chartKitTheme, props }), [chartKitTheme, props]); const { cells, monthLabels, resolvedTheme, weekdayLabels } = model; const activeCell = props.activeCell === false ? undefined : props.activeCell; const activeCellDateKey = getDateKey(activeCell?.date); const showMonthLabels = props.showMonthLabels !== false; const showWeekdayLabels = props.showWeekdayLabels !== false; const accessibilityLabel = props.accessibilityLabel ?? getContributionGraphAccessibilitySummary({ accessor: props.accessor, endDate: props.endDate, numDays: props.numDays, values: props.values }); const renderer = getContributionGraphRenderer(props.renderer ?? chartKitTheme.renderer); const Layer = renderer.Layer ?? RendererLayer; const Rect = renderer.Rect; const Surface = renderer.Surface; const SvgText = renderer.Text; const canRenderText = renderer.capabilities?.text !== false; const hasSelectionHandler = props.onDayPress !== undefined || interactionConfig.onSelect !== undefined; const isTapEnabled = interactionConfig.mode === "tap" && hasSelectionHandler; const isPressAndDragEnabled = interactionConfig.mode === "pressAndDrag" && hasSelectionHandler; const isActiveCell = useCallback((cell) => { if (!activeCell) { return false; } if (activeCell.index !== undefined && cell.index === activeCell.index) { return true; } return (activeCellDateKey !== undefined && getDateKey(cell.date) === activeCellDateKey); }, [activeCell, activeCellDateKey]); const activeCells = useMemo(() => (activeCell ? cells.filter(isActiveCell) : []), [activeCell, cells, isActiveCell]); const emitCellSelection = useCallback((cell, options = {}) => { const cellKey = getContributionGraphCellKey(cell); if (options.dedupe && lastSelectedCellKeyRef.current === cellKey) { return; } lastSelectedCellKeyRef.current = cellKey; const selectEvent = buildContributionGraphDayPressEvent(cell); props.onDayPress?.(selectEvent); interactionConfig.onSelect?.(selectEvent); }, [interactionConfig, props.onDayPress]); const selectCellAtEventLocation = useCallback((event) => { event.preventDefault(); const { locationX, locationY } = event.nativeEvent; const cell = getContributionGraphCellAtPoint({ cells, hitSlop: interactionConfig.hitSlop, locationX: locationX + interactionConfig.pointerOffset.x, locationY: locationY + interactionConfig.pointerOffset.y }); if (cell) { emitCellSelection(cell, { dedupe: true }); } }, [ cells, emitCellSelection, interactionConfig.hitSlop, interactionConfig.pointerOffset.x, interactionConfig.pointerOffset.y ]); const resetDraggedCell = useCallback(() => { lastSelectedCellKeyRef.current = undefined; }, []); const responderProps = isPressAndDragEnabled ? { onStartShouldSetResponderCapture: () => true, onMoveShouldSetResponderCapture: () => true, onStartShouldSetResponder: () => true, onMoveShouldSetResponder: () => true, onResponderGrant: (event) => { resetDraggedCell(); selectCellAtEventLocation(event); }, onResponderMove: selectCellAtEventLocation, onResponderRelease: resetDraggedCell, onResponderTerminate: resetDraggedCell, onResponderTerminationRequest: () => false } : {}; return (_jsx(View, { accessible: true, accessibilityLabel: accessibilityLabel, accessibilityRole: "image", style: [ styles.container, { backgroundColor: resolvedTheme.background, height: props.height, width: props.width } ], testID: props.testID, ...responderProps, children: _jsxs(Surface, { width: props.width, height: props.height, children: [_jsxs(Layer, { name: "axes", children: [showMonthLabels && canRenderText ? monthLabels.map((label) => { const text = getContributionGraphMonthLabel(label.monthIndex, label.date, props.getMonthLabel); return (_jsx(SvgText, { fill: resolvedTheme.mutedText, fontSize: 10, fontWeight: "700", text: text, x: label.x, y: label.y, ...(resolvedTheme.typography.fontFamily ? { fontFamily: resolvedTheme.typography.fontFamily } : {}), children: text }, label.key)); }) : null, showWeekdayLabels && canRenderText ? weekdayLabels.map((label) => { const text = getContributionGraphWeekdayLabel(label.dayIndex, props.getWeekdayLabel); return (_jsx(SvgText, { fill: resolvedTheme.mutedText, fontSize: 9, fontWeight: "700", text: text, x: label.x, y: label.y, ...(resolvedTheme.typography.fontFamily ? { fontFamily: resolvedTheme.typography.fontFamily } : {}), children: text }, label.key)); }) : null] }), _jsxs(Layer, { name: "data", children: [cells.map((cell) => (_jsx(Rect, { fill: cell.fill, height: cell.size, opacity: cell.opacity, rx: 3, ry: 3, testID: `${props.testID ?? "contribution-graph"}-cell.${cell.index}`, width: cell.size, x: cell.x, y: cell.y, onPress: isTapEnabled ? () => emitCellSelection(cell) : undefined }, `${cell.date.toISOString()}-${cell.weekIndex}-${cell.weekdayIndex}`))), activeCells.map((cell) => { const geometry = getActiveCellGeometry({ activeCell, cell }); return (_jsx(Rect, { fill: cell.fill, height: geometry.size, opacity: activeCell?.opacity ?? cell.opacity, rx: geometry.rx, ry: geometry.rx, stroke: activeCell?.strokeColor, strokeWidth: activeCell?.strokeWidth, testID: `${props.testID ?? "contribution-graph"}-cell.${cell.index}.active`, width: geometry.size, x: geometry.x, y: geometry.y, onPress: isTapEnabled ? () => emitCellSelection(cell) : undefined }, `${cell.date.toISOString()}-${cell.weekIndex}-${cell.weekdayIndex}-active`)); })] })] }) })); }; export const CalendarHeatmap = ContributionGraph; const styles = StyleSheet.create({ container: { borderRadius: 8, overflow: "hidden" } });