UNPKG

react-native-ballast-charts

Version:

High-performance interactive charts for React Native with advanced gap handling and financial data visualization

297 lines (249 loc) 10.9 kB
/** * Data Scaling Hook * Core calculation logic for coordinate transformations */ import { useMemo } from 'react'; import { detectGaps } from '../utils/gapDetection'; import { ChartData, Rectangle, Range, ScaledPoint, UseDataScalingReturn, GapConfig, Gap } from '../utils/types'; interface UseDataScalingProps { data: ChartData; yRange?: Range; chartArea: Rectangle; gaps?: GapConfig; } /** * Scale data from data space to view space coordinates * Handles uneven time spacing and provides transformation functions */ export const useDataScaling = ({ data, chartArea, gaps, yRange: yRangeOverride }: UseDataScalingProps): UseDataScalingReturn => { return useMemo(() => { const { x: xData, y: yData } = data; // // DEBUG: Print raw input data when fixed width gaps are enabled // if (gaps?.enabled && gaps?.fixedWidthGaps) { // console.log('RAW INPUT DATA:'); // console.log('xData (timestamps):', xData); // console.log('yData (prices):', yData); // console.log('chartArea:', chartArea); // console.log('gaps config:', gaps); // } // Calculate data ranges const xRange: Range = { min: Math.min(...xData), max: Math.max(...xData) }; // Use provided yRange override or calculate from data let yRange: Range; if (yRangeOverride) { yRange = { ...yRangeOverride }; } else { yRange = { min: Math.min(...yData), max: Math.max(...yData) }; // Add padding to Y range for better visualization (only when auto-calculated) const yPadding = (yRange.max - yRange.min) * 0.05; // 5% padding yRange.min -= yPadding; yRange.max += yPadding; } // Detect gaps if fixed width gaps are enabled // Detect gaps if fixed width gaps are enabled let detectedGaps: Gap[] = []; if (gaps?.enabled && gaps?.fixedWidthGaps) { detectedGaps = detectGaps(xData, gaps.threshold); // console.log(`Fixed width gaps enabled: ${detectedGaps.length} gaps detected`, detectedGaps); } // Calculate scaling with fixed width gaps const createScalingFunctions = () => { if (!gaps?.fixedWidthGaps || detectedGaps.length === 0) { // Standard linear scaling const scaleX = (value: number): number => { const xSpan = xRange.max - xRange.min; if (xSpan === 0) return chartArea.x + chartArea.width / 2; return chartArea.x + ((value - xRange.min) / xSpan) * chartArea.width; }; const unscaleX = (coordinate: number): number => { const xSpan = xRange.max - xRange.min; if (xSpan === 0) return xRange.min; return xRange.min + ((coordinate - chartArea.x) / chartArea.width) * xSpan; }; return { scaleX, unscaleX }; } // Fixed width gap scaling const fixedGapWidth = gaps.fixedWidth || 40; const totalGapVisualWidth = detectedGaps.length * fixedGapWidth; const dataWidth = chartArea.width - totalGapVisualWidth; // Calculate total data time span (excluding gap durations) let totalDataTimeSpan = 0; for (let i = 0; i <= detectedGaps.length; i++) { if (i === 0) { // First segment: from data start to first gap start (or end if no gaps) const segmentEnd = detectedGaps.length > 0 ? detectedGaps[0].startX : xRange.max; totalDataTimeSpan += (segmentEnd - xRange.min); } else if (i === detectedGaps.length) { // Final segment: from last gap end to data end const segmentStart = detectedGaps[i - 1].endX; totalDataTimeSpan += (xRange.max - segmentStart); } else { // Middle segments: from previous gap end to current gap start const segmentStart = detectedGaps[i - 1].endX; const segmentEnd = detectedGaps[i].startX; totalDataTimeSpan += (segmentEnd - segmentStart); } } // Calculate consistent time-to-pixel ratio across all segments const timeToPixelRatio = dataWidth / totalDataTimeSpan; // console.log(`Fixed width scaling: ${detectedGaps.length} gaps, ${fixedGapWidth}px each, dataWidth: ${dataWidth}, totalWidth: ${chartArea.width}`); // console.log(`Total data time span: ${totalDataTimeSpan}, time-to-pixel ratio: ${timeToPixelRatio}`); const scaleX = (value: number): number => { if (totalDataTimeSpan === 0) return chartArea.x + chartArea.width / 2; // Find which segment this value belongs to and calculate position let cumulativePixels = 0; let cumulativeGaps = 0; // Check each segment for (let i = 0; i <= detectedGaps.length; i++) { let segmentStart: number; let segmentEnd: number; if (i === 0) { // First segment: from data start to first gap start (or end if no gaps) segmentStart = xRange.min; segmentEnd = detectedGaps.length > 0 ? detectedGaps[0].startX : xRange.max; } else if (i === detectedGaps.length) { // Final segment: from last gap end to data end segmentStart = detectedGaps[i - 1].endX; segmentEnd = xRange.max; } else { // Middle segments: from previous gap end to current gap start segmentStart = detectedGaps[i - 1].endX; segmentEnd = detectedGaps[i].startX; } if (value >= segmentStart && value <= segmentEnd) { // Value is in this segment - calculate position within segment const segmentPosition = (value - segmentStart) * timeToPixelRatio; return chartArea.x + cumulativePixels + segmentPosition + (cumulativeGaps * fixedGapWidth); } // Add this segment's width to cumulative total const segmentTimeSpan = segmentEnd - segmentStart; cumulativePixels += segmentTimeSpan * timeToPixelRatio; // Add gap after this segment (except for the last segment) if (i < detectedGaps.length) { cumulativeGaps++; } } // Fallback (shouldn't reach here) // console.warn('Value not found in any segment:', value, 'xRange:', xRange); return chartArea.x; }; const unscaleX = (coordinate: number): number => { if (totalDataTimeSpan === 0) return xRange.min; // Remove chart area offset const relativeCoord = coordinate - chartArea.x; // Find which segment this coordinate falls in let cumulativePixels = 0; let cumulativeGaps = 0; // Check each segment for (let i = 0; i <= detectedGaps.length; i++) { let segmentStart: number; let segmentEnd: number; if (i === 0) { segmentStart = xRange.min; segmentEnd = detectedGaps.length > 0 ? detectedGaps[0].startX : xRange.max; } else if (i === detectedGaps.length) { segmentStart = detectedGaps[i - 1].endX; segmentEnd = xRange.max; } else { segmentStart = detectedGaps[i - 1].endX; segmentEnd = detectedGaps[i].startX; } const segmentTimeSpan = segmentEnd - segmentStart; const segmentPixelWidth = segmentTimeSpan * timeToPixelRatio; const segmentStartPixel = cumulativePixels + (cumulativeGaps * fixedGapWidth); const segmentEndPixel = segmentStartPixel + segmentPixelWidth; if (relativeCoord >= segmentStartPixel && relativeCoord <= segmentEndPixel) { // Coordinate is in this segment const positionInSegment = relativeCoord - segmentStartPixel; const timeInSegment = positionInSegment / timeToPixelRatio; return segmentStart + timeInSegment; } // Add this segment's width cumulativePixels += segmentPixelWidth; // Check if coordinate is in the gap after this segment if (i < detectedGaps.length) { const gapStartPixel = segmentEndPixel; const gapEndPixel = gapStartPixel + fixedGapWidth; if (relativeCoord >= gapStartPixel && relativeCoord <= gapEndPixel) { // Coordinate is within the gap - return gap start time return segmentEnd; } cumulativeGaps++; } } // Fallback // console.warn('Coordinate not found in any segment:', relativeCoord); return xRange.min; }; return { scaleX, unscaleX }; }; const { scaleX, unscaleX } = createScalingFunctions(); // Standard Y scaling (unchanged) const scaleY = (value: number): number => { const ySpan = yRange.max - yRange.min; if (ySpan === 0) return chartArea.y + chartArea.height / 2; // Invert Y axis (SVG coordinates start from top) return chartArea.y + chartArea.height - ((value - yRange.min) / ySpan) * chartArea.height; }; const unscaleY = (coordinate: number): number => { const ySpan = yRange.max - yRange.min; if (ySpan === 0) return yRange.min; // Invert Y axis return yRange.min + ((chartArea.y + chartArea.height - coordinate) / chartArea.height) * ySpan; }; // Transform all data points to scaled coordinates const scaledData: ScaledPoint[] = xData.map((x, index) => ({ x: scaleX(x), y: scaleY(yData[index]), originalX: x, originalY: yData[index], index })); // // DEBUG: Print scaled X coordinates when fixed width gaps are enabled // if (gaps?.enabled && gaps?.fixedWidthGaps) { // console.log('SCALED X COORDINATES:'); // console.log('First 10 points:', scaledData.slice(0, 10).map(p => ({ originalX: p.originalX, scaledX: p.x }))); // console.log('Last 10 points:', scaledData.slice(-10).map(p => ({ originalX: p.originalX, scaledX: p.x }))); // console.log('Points around detected gaps:'); // detectedGaps.forEach((gap, gapIndex) => { // console.log(`Gap ${gapIndex + 1} (${gap.startX} to ${gap.endX}):`); // const beforeGap = scaledData.find(p => p.originalX === gap.startX); // const afterGap = scaledData.find(p => p.originalX === gap.endX); // console.log(` Before gap (${gap.startX}): x=${beforeGap?.x}`); // console.log(` After gap (${gap.endX}): x=${afterGap?.x}`); // if (beforeGap && afterGap) { // console.log(` Visual gap width: ${afterGap.x - beforeGap.x}px`); // } // }); // } return { scaledData, xRange, yRange, scaleX, scaleY, unscaleX, unscaleY }; }, [data, chartArea, gaps]); }; ;