UNPKG

react-native-stock-graphs

Version:

High-performance, interactive stock charts for React Native with TypeScript support

1,663 lines (1,654 loc) 76.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); var reactNative = require('react-native'); var Svg = require('react-native-svg'); function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: true, configurable: true, writable: true }) : e[r] = t, e; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } // Implementation function timeToX(timestamp, timeRange, chartWidthOrDimensions, padding = { left: 0, right: 0 }) { const { min, max } = timeRange; let chartWidth; let actualPadding; if (typeof chartWidthOrDimensions === 'object') { // ChartDimensions object chartWidth = chartWidthOrDimensions.width; actualPadding = { left: chartWidthOrDimensions.paddingLeft, right: chartWidthOrDimensions.paddingRight }; } else { // Individual parameters chartWidth = chartWidthOrDimensions; actualPadding = padding; } const availableWidth = chartWidth - actualPadding.left - actualPadding.right; const ratio = (timestamp - min) / (max - min); return actualPadding.left + ratio * availableWidth; } // Implementation function priceToY(price, priceRange, chartHeightOrDimensions, padding = { top: 0, bottom: 0 }) { const { min, max } = priceRange; let chartHeight; let actualPadding; if (typeof chartHeightOrDimensions === 'object') { // ChartDimensions object chartHeight = chartHeightOrDimensions.height; actualPadding = { top: chartHeightOrDimensions.paddingTop, bottom: chartHeightOrDimensions.paddingBottom }; } else { // Individual parameters chartHeight = chartHeightOrDimensions; actualPadding = padding; } const availableHeight = chartHeight - actualPadding.top - actualPadding.bottom; const ratio = (max - price) / (max - min); return actualPadding.top + ratio * availableHeight; } // Implementation function xToTime(x, timeRange, chartWidthOrDimensions, padding = { left: 0, right: 0 }) { const { min, max } = timeRange; let chartWidth; let actualPadding; if (typeof chartWidthOrDimensions === 'object') { // ChartDimensions object chartWidth = chartWidthOrDimensions.width; actualPadding = { left: chartWidthOrDimensions.paddingLeft, right: chartWidthOrDimensions.paddingRight }; } else { // Individual parameters chartWidth = chartWidthOrDimensions; actualPadding = padding; } const availableWidth = chartWidth - actualPadding.left - actualPadding.right; const ratio = (x - actualPadding.left) / availableWidth; return min + ratio * (max - min); } // Implementation function yToPrice(y, priceRange, chartHeightOrDimensions, padding = { top: 0, bottom: 0 }) { const { min, max } = priceRange; let chartHeight; let actualPadding; if (typeof chartHeightOrDimensions === 'object') { // ChartDimensions object chartHeight = chartHeightOrDimensions.height; actualPadding = { top: chartHeightOrDimensions.paddingTop, bottom: chartHeightOrDimensions.paddingBottom }; } else { // Individual parameters chartHeight = chartHeightOrDimensions; actualPadding = padding; } const availableHeight = chartHeight - actualPadding.top - actualPadding.bottom; // Invert Y coordinate const ratio = (chartHeight - actualPadding.bottom - y) / availableHeight; return min + ratio * (max - min); } /** * Get time range from data */ const getTimeRange = data => { if (data.length === 0) return { min: 0, max: 0 }; const times = data.map(item => item.time); return { min: Math.min(...times), max: Math.max(...times) }; }; function getPriceRange(data) { if (data.length === 0) return { min: 0, max: 0 }; let min = Infinity; let max = -Infinity; data.forEach(item => { if ('high' in item && 'low' in item) { // CandleData min = Math.min(min, item.low); max = Math.max(max, item.high); } else if ('value' in item) { // LineData min = Math.min(min, item.value); max = Math.max(max, item.value); } }); return { min, max }; } /** * Get value range from line data */ const getValueRange = data => { if (data.length === 0) return { min: 0, max: 0 }; const values = data.map(item => item.value); return { min: Math.min(...values), max: Math.max(...values) }; }; /** * Filter data by date range */ const filterDataByRange = (data, range) => { let minTime; let maxTime; if ('from' in range && 'to' in range) { // DateRange format minTime = typeof range.from === 'string' ? new Date(range.from).getTime() : range.from; maxTime = typeof range.to === 'string' ? new Date(range.to).getTime() : range.to; } else { // { min, max } format minTime = range.min; maxTime = range.max; } return data.filter(item => item.time >= minTime && item.time <= maxTime); }; /** * Decimate data for performance (keep every nth point) */ const decimateData = (data, maxPoints) => { if (data.length <= maxPoints) return data; const step = Math.ceil(data.length / maxPoints); const decimated = []; for (let i = 0; i < data.length; i += step) { decimated.push(data[i]); } // Always include the last point if (decimated[decimated.length - 1] !== data[data.length - 1]) { decimated.push(data[data.length - 1]); } return decimated; }; // Implementation function findNearestDataPoint(data, xOrTime, timeRange, chartWidth, padding = { left: 0, right: 0 }) { if (data.length === 0) return null; let targetTime; // If timeRange is provided, treat xOrTime as x coordinate if (timeRange && chartWidth !== undefined) { targetTime = xToTime(xOrTime, timeRange, chartWidth, padding); } else { // Otherwise, treat xOrTime as timestamp targetTime = xOrTime; } let closestIndex = 0; let closestDistance = Math.abs(data[0].time - targetTime); for (let i = 1; i < data.length; i++) { const distance = Math.abs(data[i].time - targetTime); if (distance < closestDistance) { closestDistance = distance; closestIndex = i; } } const result = { point: data[closestIndex], index: closestIndex }; // Add time property for timestamp-based calls if (!timeRange) { result.time = data[closestIndex].time; } return result; } /** * Generate nice tick values for axis */ const generateTicks = (min, max, maxTicks = 5) => { if (min === max) return [min]; const range = max - min; const roughStep = range / (maxTicks - 1); // Find a "nice" step size const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep))); const normalizedStep = roughStep / magnitude; let niceStep; if (normalizedStep <= 1) niceStep = 1;else if (normalizedStep <= 2) niceStep = 2;else if (normalizedStep <= 5) niceStep = 5;else niceStep = 10; const step = niceStep * magnitude; // Generate ticks const ticks = []; const start = Math.ceil(min / step) * step; for (let tick = start; tick <= max; tick += step) { ticks.push(tick); } return ticks; }; /** * Format price for display */ const formatPrice = (price, decimals = 2) => { return price.toFixed(decimals); }; /** * Format timestamp for display */ const formatTime = (timestamp, format = 'date') => { const date = new Date(timestamp); switch (format) { case 'time': return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); case 'date': return date.toLocaleDateString('en-US', { month: 'short', day: '2-digit' }); case 'datetime': return date.toLocaleString(); default: return date.toISOString(); } }; /** * Calculate visible data range based on zoom and pan */ const calculateVisibleRange = (data, zoom, panX, chartWidth) => { if (data.length === 0) { return { startIndex: 0, endIndex: 0, visibleData: [] }; } const totalWidth = chartWidth * zoom; const visibleWidth = chartWidth; const startRatio = Math.max(0, panX / totalWidth); const endRatio = Math.min(1, (panX + visibleWidth) / totalWidth); const startIndex = Math.floor(startRatio * data.length); const endIndex = Math.min(data.length - 1, Math.ceil(endRatio * data.length)); return { startIndex, endIndex, visibleData: data.slice(startIndex, endIndex + 1) }; }; /** * Clamp value between min and max */ const clamp = (value, min, max) => { return Math.min(Math.max(value, min), max); }; /** * Linear interpolation */ const lerp = (start, end, factor) => { return start + (end - start) * factor; }; // Implementation function distance(x1OrPoint1, y1OrPoint2, x2, y2) { let x1, y1, x2Final, y2Final; if (typeof x1OrPoint1 === 'object' && typeof y1OrPoint2 === 'object') { // Point objects x1 = x1OrPoint1.x; y1 = x1OrPoint1.y; x2Final = y1OrPoint2.x; y2Final = y1OrPoint2.y; } else { // Individual coordinates x1 = x1OrPoint1; y1 = y1OrPoint2; x2Final = x2; y2Final = y2; } return Math.sqrt(Math.pow(x2Final - x1, 2) + Math.pow(y2Final - y1, 2)); } /** * Throttle function calls */ const throttle = (func, delay) => { let timeoutId = null; let lastExecTime = 0; return (...args) => { const currentTime = Date.now(); if (currentTime - lastExecTime > delay) { func(...args); lastExecTime = currentTime; } else { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => { func(...args); lastExecTime = Date.now(); }, delay - (currentTime - lastExecTime)); } }; }; /** * Debounce function calls */ const debounce = (func, delay) => { let timeoutId = null; return (...args) => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), delay); }; }; const SVGRenderer = ({ type, data, dimensions, theme, axis, timeRange, priceRange, indicators, tooltip, crosshair, zoom, pan }) => { const { width, height, paddingTop, paddingBottom, paddingLeft, paddingRight } = dimensions; const chartWidth = width - paddingLeft - paddingRight; const chartHeight = height - paddingTop - paddingBottom; // Generate axis ticks const priceTicks = generateTicks(priceRange.min, priceRange.max, axis.tickCount || 5); const timeTicks = generateTicks(timeRange.min, timeRange.max, axis.tickCount || 5); // Render grid lines const renderGrid = () => { const gridLines = []; // Horizontal grid lines (price levels) priceTicks.forEach((price, index) => { const y = priceToY(price, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); gridLines.push(jsxRuntime.jsx(Svg.Line, { x1: paddingLeft, y1: y, x2: width - paddingRight, y2: y, stroke: theme.gridColor, strokeWidth: 0.5, opacity: 0.5 }, `hgrid-${index}`)); }); // Vertical grid lines (time levels) timeTicks.forEach((time, index) => { const x = timeToX(time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); gridLines.push(jsxRuntime.jsx(Svg.Line, { x1: x, y1: paddingTop, x2: x, y2: height - paddingBottom, stroke: theme.gridColor, strokeWidth: 0.5, opacity: 0.5 }, `vgrid-${index}`)); }); return jsxRuntime.jsx(Svg.G, { children: gridLines }); }; // Render Y-axis (price axis) const renderYAxis = () => { if (!axis.showYAxis) return null; return jsxRuntime.jsx(Svg.G, { children: priceTicks.map((price, index) => { const y = priceToY(price, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); return jsxRuntime.jsx(Svg.Text, { x: width - paddingRight + 5, y: y + 4, fontSize: 12, fill: theme.textColor, textAnchor: "start", children: formatPrice(price) }, `y-tick-${index}`); }) }); }; // Render X-axis (time axis) const renderXAxis = () => { if (!axis.showXAxis) return null; return jsxRuntime.jsx(Svg.G, { children: timeTicks.map((time, index) => { const x = timeToX(time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); return jsxRuntime.jsx(Svg.Text, { x: x, y: height - paddingBottom + 15, fontSize: 12, fill: theme.textColor, textAnchor: "middle", children: formatTime(time) }, `x-tick-${index}`); }) }); }; // Render line chart const renderLineChart = lineData => { if (lineData.length < 2) return null; const pathData = lineData.map((point, index) => { const x = timeToX(point.time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const y = priceToY(point.value, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); return `${index === 0 ? 'M' : 'L'} ${x} ${y}`; }).join(' '); return jsxRuntime.jsx(Svg.Path, { d: pathData, stroke: theme.lineColor, strokeWidth: 2, fill: "none" }); }; // Render area chart const renderAreaChart = lineData => { var _theme$areaGradient, _theme$areaGradient2; if (lineData.length < 2) return null; const pathData = lineData.map((point, index) => { const x = timeToX(point.time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const y = priceToY(point.value, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); return `${index === 0 ? 'M' : 'L'} ${x} ${y}`; }).join(' '); const firstX = timeToX(lineData[0].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const lastX = timeToX(lineData[lineData.length - 1].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const bottomY = height - paddingBottom; const areaPath = `${pathData} L ${lastX} ${bottomY} L ${firstX} ${bottomY} Z`; return jsxRuntime.jsxs(Svg.G, { children: [jsxRuntime.jsx(Svg.Defs, { children: jsxRuntime.jsxs(Svg.LinearGradient, { id: "areaGradient", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [jsxRuntime.jsx(Svg.Stop, { offset: "0%", stopColor: ((_theme$areaGradient = theme.areaGradient) === null || _theme$areaGradient === void 0 ? void 0 : _theme$areaGradient.start) || theme.lineColor, stopOpacity: 0.8 }), jsxRuntime.jsx(Svg.Stop, { offset: "100%", stopColor: ((_theme$areaGradient2 = theme.areaGradient) === null || _theme$areaGradient2 === void 0 ? void 0 : _theme$areaGradient2.end) || theme.lineColor, stopOpacity: 0.1 })] }) }), jsxRuntime.jsx(Svg.Path, { d: areaPath, fill: "url(#areaGradient)" }), jsxRuntime.jsx(Svg.Path, { d: pathData, stroke: theme.lineColor, strokeWidth: 2, fill: "none" })] }); }; // Render candlestick chart const renderCandlestickChart = candleData => { return jsxRuntime.jsx(Svg.G, { children: candleData.map((candle, index) => { const x = timeToX(candle.time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const openY = priceToY(candle.open, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const highY = priceToY(candle.high, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const lowY = priceToY(candle.low, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const closeY = priceToY(candle.close, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const isUp = candle.close >= candle.open; const color = isUp ? theme.candleUp : theme.candleDown; const candleWidth = Math.max(1, chartWidth / candleData.length * 0.8); return jsxRuntime.jsxs(Svg.G, { children: [jsxRuntime.jsx(Svg.Line, { x1: x, y1: highY, x2: x, y2: lowY, stroke: color, strokeWidth: 1 }), jsxRuntime.jsx(Svg.Rect, { x: x - candleWidth / 2, y: Math.min(openY, closeY), width: candleWidth, height: Math.abs(closeY - openY) || 1, fill: isUp ? color : color, fillOpacity: isUp ? 0.8 : 1, stroke: color, strokeWidth: 1 })] }, `candle-${index}`); }) }); }; // Render OHLC chart const renderOHLCChart = candleData => { return jsxRuntime.jsx(Svg.G, { children: candleData.map((candle, index) => { const x = timeToX(candle.time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const openY = priceToY(candle.open, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const highY = priceToY(candle.high, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const lowY = priceToY(candle.low, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const closeY = priceToY(candle.close, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const isUp = candle.close >= candle.open; const color = isUp ? theme.candleUp : theme.candleDown; const tickWidth = 3; return jsxRuntime.jsxs(Svg.G, { children: [jsxRuntime.jsx(Svg.Line, { x1: x, y1: highY, x2: x, y2: lowY, stroke: color, strokeWidth: 1 }), jsxRuntime.jsx(Svg.Line, { x1: x - tickWidth, y1: openY, x2: x, y2: openY, stroke: color, strokeWidth: 1 }), jsxRuntime.jsx(Svg.Line, { x1: x, y1: closeY, x2: x + tickWidth, y2: closeY, stroke: color, strokeWidth: 1 })] }, `ohlc-${index}`); }) }); }; // Render indicators const renderIndicators = () => { return jsxRuntime.jsx(Svg.G, { children: indicators.map((indicator, indicatorIndex) => { if (indicator.data.length === 0) return null; const pathData = indicator.data.map((value, index) => { if (index >= data.length) return ''; const dataPoint = data[index]; const x = timeToX(dataPoint.time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const y = priceToY(value, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); return `${index === 0 ? 'M' : 'L'} ${x} ${y}`; }).join(' '); return jsxRuntime.jsx(Svg.Path, { d: pathData, stroke: indicator.config.color || '#ff9800', strokeWidth: 1, fill: "none", opacity: 0.8 }, `indicator-${indicatorIndex}`); }) }); }; // Render crosshair const renderCrosshair = () => { if (!crosshair.visible) return null; return jsxRuntime.jsxs(Svg.G, { children: [jsxRuntime.jsx(Svg.Line, { x1: crosshair.x, y1: paddingTop, x2: crosshair.x, y2: height - paddingBottom, stroke: theme.crosshairColor, strokeWidth: 1, strokeDasharray: "3,3" }), jsxRuntime.jsx(Svg.Line, { x1: paddingLeft, y1: crosshair.y, x2: width - paddingRight, y2: crosshair.y, stroke: theme.crosshairColor, strokeWidth: 1, strokeDasharray: "3,3" })] }); }; // Render tooltip const renderTooltip = () => { if (!tooltip.visible || !tooltip.candle && !tooltip.point) return null; const tooltipWidth = 120; const tooltipHeight = tooltip.candle ? 80 : 40; const tooltipX = Math.min(tooltip.x + 10, width - tooltipWidth - 10); const tooltipY = Math.max(tooltip.y - tooltipHeight - 10, 10); return jsxRuntime.jsxs(Svg.G, { children: [jsxRuntime.jsx(Svg.Rect, { x: tooltipX, y: tooltipY, width: tooltipWidth, height: tooltipHeight, fill: theme.tooltipBackground, rx: 4, opacity: 0.9 }), tooltip.candle && jsxRuntime.jsxs(Svg.G, { children: [jsxRuntime.jsxs(Svg.Text, { x: tooltipX + 5, y: tooltipY + 15, fontSize: 10, fill: theme.tooltipText, children: ["O: ", formatPrice(tooltip.candle.open)] }), jsxRuntime.jsxs(Svg.Text, { x: tooltipX + 5, y: tooltipY + 30, fontSize: 10, fill: theme.tooltipText, children: ["H: ", formatPrice(tooltip.candle.high)] }), jsxRuntime.jsxs(Svg.Text, { x: tooltipX + 5, y: tooltipY + 45, fontSize: 10, fill: theme.tooltipText, children: ["L: ", formatPrice(tooltip.candle.low)] }), jsxRuntime.jsxs(Svg.Text, { x: tooltipX + 5, y: tooltipY + 60, fontSize: 10, fill: theme.tooltipText, children: ["C: ", formatPrice(tooltip.candle.close)] })] }), tooltip.point && jsxRuntime.jsxs(Svg.Text, { x: tooltipX + 5, y: tooltipY + 20, fontSize: 10, fill: theme.tooltipText, children: ["Value: ", formatPrice(tooltip.point.value)] })] }); }; // Main render function const renderChart = () => { if (data.length === 0) return null; switch (type) { case 'line': return renderLineChart(data); case 'area': return renderAreaChart(data); case 'candlestick': return renderCandlestickChart(data); case 'ohlc': return renderOHLCChart(data); default: return null; } }; return jsxRuntime.jsxs(Svg, { width: width, height: height, children: [renderGrid(), renderChart(), renderIndicators(), renderCrosshair(), renderYAxis(), renderXAxis(), renderTooltip()] }); }; // Try to import Skia components, fallback to placeholder if not available let Canvas; let Path; let Line; let Rect; let Circle; let SkiaText; let LinearGradient; let vec; let Skia; try { const SkiaModule = require('@shopify/react-native-skia'); Canvas = SkiaModule.Canvas; Path = SkiaModule.Path; Line = SkiaModule.Line; Rect = SkiaModule.Rect; Circle = SkiaModule.Circle; SkiaText = SkiaModule.Text; LinearGradient = SkiaModule.LinearGradient; vec = SkiaModule.vec; Skia = SkiaModule.Skia; } catch (error) { // Skia not available, will render placeholder } /** * Skia-based renderer for high-performance chart rendering */ const SkiaRenderer = ({ type, data, dimensions, theme, axis, timeRange, priceRange, indicators, tooltip, crosshair, zoom, pan }) => { const { width, height, paddingTop, paddingBottom, paddingLeft, paddingRight } = dimensions; const chartWidth = width - paddingLeft - paddingRight; const chartHeight = height - paddingTop - paddingBottom; // If Skia is not available, render placeholder if (!Canvas) { return jsxRuntime.jsxs(reactNative.View, { style: { width: dimensions.width, height: dimensions.height, backgroundColor: theme.background, justifyContent: 'center', alignItems: 'center', borderWidth: 1, borderColor: theme.gridColor, borderStyle: 'dashed' }, children: [jsxRuntime.jsx(reactNative.Text, { style: { color: theme.textColor, fontSize: 16, fontWeight: 'bold' }, children: "Skia Renderer" }), jsxRuntime.jsxs(reactNative.Text, { style: { color: theme.textColor, fontSize: 12, marginTop: 8, textAlign: 'center' }, children: ["High-performance rendering with", "\n", "@shopify/react-native-skia"] }), jsxRuntime.jsx(reactNative.Text, { style: { color: theme.textColor, fontSize: 10, marginTop: 8, opacity: 0.7 }, children: "Install react-native-skia to enable" })] }); } // Generate axis ticks const priceTicks = react.useMemo(() => generateTicks(priceRange.min, priceRange.max, axis.tickCount || 5), [priceRange, axis.tickCount]); const timeTicks = react.useMemo(() => generateTicks(timeRange.min, timeRange.max, axis.tickCount || 5), [timeRange, axis.tickCount]); // Render grid lines const renderGrid = () => { const gridElements = []; // Horizontal grid lines (price levels) priceTicks.forEach((price, index) => { const y = priceToY(price, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); gridElements.push(jsxRuntime.jsx(Line, { p1: vec(paddingLeft, y), p2: vec(width - paddingRight, y), color: theme.gridColor, style: "stroke", strokeWidth: 0.5, opacity: 0.5 }, `hgrid-${index}`)); }); // Vertical grid lines (time levels) timeTicks.forEach((time, index) => { const x = timeToX(time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); gridElements.push(jsxRuntime.jsx(Line, { p1: vec(x, paddingTop), p2: vec(x, height - paddingBottom), color: theme.gridColor, style: "stroke", strokeWidth: 0.5, opacity: 0.5 }, `vgrid-${index}`)); }); return gridElements; }; // Render line chart const renderLineChart = () => { if (data.length < 2) return null; const path = Skia.Path.Make(); const lineData = data; // Move to first point const firstX = timeToX(lineData[0].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const firstY = priceToY(lineData[0].value, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); path.moveTo(firstX, firstY); // Draw lines to subsequent points for (let i = 1; i < lineData.length; i++) { const x = timeToX(lineData[i].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const y = priceToY(lineData[i].value, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); path.lineTo(x, y); } return jsxRuntime.jsx(Path, { path: path, color: theme.lineColor, style: "stroke", strokeWidth: 2, strokeCap: "round", strokeJoin: "round" }); }; // Render area chart const renderAreaChart = () => { var _theme$areaGradient; if (data.length < 2) return null; const path = Skia.Path.Make(); const lineData = data; // Move to first point const firstX = timeToX(lineData[0].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const firstY = priceToY(lineData[0].value, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); path.moveTo(firstX, height - paddingBottom); path.lineTo(firstX, firstY); // Draw lines to subsequent points for (let i = 1; i < lineData.length; i++) { const x = timeToX(lineData[i].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const y = priceToY(lineData[i].value, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); path.lineTo(x, y); } // Close the path const lastX = timeToX(lineData[lineData.length - 1].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); path.lineTo(lastX, height - paddingBottom); path.close(); return jsxRuntime.jsx(Path, { path: path, color: ((_theme$areaGradient = theme.areaGradient) === null || _theme$areaGradient === void 0 ? void 0 : _theme$areaGradient.start) || theme.lineColor, opacity: 0.3 }); }; // Render candlestick chart const renderCandlestickChart = () => { const candleData = data; const candleElements = []; candleData.forEach((candle, index) => { const x = timeToX(candle.time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const openY = priceToY(candle.open, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const highY = priceToY(candle.high, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const lowY = priceToY(candle.low, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const closeY = priceToY(candle.close, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const isUp = candle.close >= candle.open; const color = isUp ? theme.candleUp : theme.candleDown; const candleWidth = Math.max(1, chartWidth / candleData.length * 0.8); // High-Low line (wick) candleElements.push(jsxRuntime.jsx(Line, { p1: vec(x, highY), p2: vec(x, lowY), color: color, strokeWidth: 1 }, `wick-${index}`)); // Open-Close rectangle (body) const bodyTop = Math.min(openY, closeY); const bodyHeight = Math.abs(closeY - openY); candleElements.push(jsxRuntime.jsx(Rect, { x: x - candleWidth / 2, y: bodyTop, width: candleWidth, height: Math.max(1, bodyHeight), color: color }, `body-${index}`)); }); return candleElements; }; // Render OHLC chart const renderOHLCChart = () => { const candleData = data; const ohlcElements = []; candleData.forEach((candle, index) => { const x = timeToX(candle.time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const openY = priceToY(candle.open, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const highY = priceToY(candle.high, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const lowY = priceToY(candle.low, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const closeY = priceToY(candle.close, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); const isUp = candle.close >= candle.open; const color = isUp ? theme.candleUp : theme.candleDown; const tickWidth = Math.max(2, chartWidth / candleData.length * 0.3); // High-Low line ohlcElements.push(jsxRuntime.jsx(Line, { p1: vec(x, highY), p2: vec(x, lowY), color: color, strokeWidth: 1 }, `hl-${index}`)); // Open tick (left) ohlcElements.push(jsxRuntime.jsx(Line, { p1: vec(x - tickWidth, openY), p2: vec(x, openY), color: color, strokeWidth: 1 }, `open-${index}`)); // Close tick (right) ohlcElements.push(jsxRuntime.jsx(Line, { p1: vec(x, closeY), p2: vec(x + tickWidth, closeY), color: color, strokeWidth: 1 }, `close-${index}`)); }); return ohlcElements; }; // Render indicators const renderIndicators = () => { return indicators.map((indicator, indicatorIndex) => { if (!indicator.data || indicator.data.length === 0) return null; const path = Skia.Path.Make(); const color = indicator.config.color || theme.lineColor; // Move to first point if (indicator.data.length > 0) { const firstX = timeToX(data[0].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const firstY = priceToY(indicator.data[0], priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); path.moveTo(firstX, firstY); // Draw lines to subsequent points for (let i = 1; i < Math.min(indicator.data.length, data.length); i++) { const x = timeToX(data[i].time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); const y = priceToY(indicator.data[i], priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); path.lineTo(x, y); } } return jsxRuntime.jsx(Path, { path: path, color: color, style: "stroke", strokeWidth: 1.5, opacity: 0.8 }, `indicator-${indicatorIndex}`); }); }; // Render crosshair const renderCrosshair = () => { if (!crosshair.visible) return null; return [jsxRuntime.jsx(Line, { p1: vec(paddingLeft, crosshair.y), p2: vec(width - paddingRight, crosshair.y), color: theme.crosshairColor, strokeWidth: 1, opacity: 0.8 }, "crosshair-h"), jsxRuntime.jsx(Line, { p1: vec(crosshair.x, paddingTop), p2: vec(crosshair.x, height - paddingBottom), color: theme.crosshairColor, strokeWidth: 1, opacity: 0.8 }, "crosshair-v")]; }; // Render axis labels const renderAxisLabels = () => { const labels = []; if (axis.showYAxis) { // Price labels priceTicks.forEach((price, index) => { const y = priceToY(price, priceRange, chartHeight, { top: paddingTop, bottom: paddingBottom }); labels.push(jsxRuntime.jsx(SkiaText, { x: width - paddingRight + 5, y: y + 4, text: formatPrice(price), color: theme.textColor, size: 10 }, `price-label-${index}`)); }); } if (axis.showXAxis) { // Time labels timeTicks.forEach((time, index) => { const x = timeToX(time, timeRange, chartWidth, { left: paddingLeft, right: paddingRight }); labels.push(jsxRuntime.jsx(SkiaText, { x: x, y: height - paddingBottom + 15, text: formatTime(time), color: theme.textColor, size: 10 }, `time-label-${index}`)); }); } return labels; }; // Render tooltip const renderTooltip = () => { if (!tooltip.visible || !tooltip.candle && !tooltip.point) return null; const tooltipWidth = 120; const tooltipHeight = tooltip.candle ? 80 : 40; const tooltipX = Math.min(tooltip.x + 10, width - tooltipWidth - 10); const tooltipY = Math.max(tooltip.y - tooltipHeight - 10, 10); const tooltipElements = []; // Tooltip background tooltipElements.push(jsxRuntime.jsx(Rect, { x: tooltipX, y: tooltipY, width: tooltipWidth, height: tooltipHeight, color: theme.tooltipBackground, opacity: 0.9 }, "tooltip-bg")); // Tooltip content if (tooltip.candle) { tooltipElements.push(jsxRuntime.jsx(SkiaText, { x: tooltipX + 5, y: tooltipY + 15, text: `O: ${formatPrice(tooltip.candle.open)}`, color: theme.tooltipText, size: 10 }, "tooltip-open"), jsxRuntime.jsx(SkiaText, { x: tooltipX + 5, y: tooltipY + 30, text: `H: ${formatPrice(tooltip.candle.high)}`, color: theme.tooltipText, size: 10 }, "tooltip-high"), jsxRuntime.jsx(SkiaText, { x: tooltipX + 5, y: tooltipY + 45, text: `L: ${formatPrice(tooltip.candle.low)}`, color: theme.tooltipText, size: 10 }, "tooltip-low"), jsxRuntime.jsx(SkiaText, { x: tooltipX + 5, y: tooltipY + 60, text: `C: ${formatPrice(tooltip.candle.close)}`, color: theme.tooltipText, size: 10 }, "tooltip-close")); } if (tooltip.point) { tooltipElements.push(jsxRuntime.jsx(SkiaText, { x: tooltipX + 5, y: tooltipY + 20, text: `Value: ${formatPrice(tooltip.point.value)}`, color: theme.tooltipText, size: 10 }, "tooltip-value")); } return tooltipElements; }; // Main chart rendering const renderChart = () => { switch (type) { case 'line': return renderLineChart(); case 'area': return [renderAreaChart(), renderLineChart()]; case 'candlestick': return renderCandlestickChart(); case 'ohlc': return renderOHLCChart(); default: return null; } }; return jsxRuntime.jsxs(Canvas, { style: { width, height }, children: [jsxRuntime.jsx(Rect, { x: 0, y: 0, width: width, height: height, color: theme.background }), renderGrid(), renderChart(), renderIndicators(), renderCrosshair(), renderAxisLabels(), renderTooltip()] }); }; /** * Calculate Simple Moving Average (SMA) */ const calculateSMA = (data, period) => { if (data.length < period) return []; // Handle different input types const values = data.map(item => { if (typeof item === 'number') { return item; } else if (typeof item === 'object' && item !== null && 'close' in item) { return item.close; } else { return item.value; } }); const sma = []; // Add null values for the first period-1 entries for (let i = 0; i < period - 1; i++) { sma.push(null); } for (let i = period - 1; i < values.length; i++) { const sum = values.slice(i - period + 1, i + 1).reduce((acc, val) => acc + val, 0); sma.push(sum / period); } return sma; }; /** * Calculate Exponential Moving Average (EMA) */ const calculateEMA = (data, period, smoothing = 2) => { if (data.length === 0) return []; // Handle different input types const values = data.map(item => { if (typeof item === 'number') { return item; } else if (typeof item === 'object' && item !== null && 'close' in item) { return item.close; } else { return item.value; } }); const ema = []; const multiplier = smoothing / (period + 1); // For period 1, return the original values if (period === 1) { return values; } // Start with the first value ema.push(values[0]); // Calculate EMA for remaining values for (let i = 1; i < values.length; i++) { const prevEMA = ema[i - 1]; if (prevEMA !== null) { const currentEMA = values[i] * multiplier + prevEMA * (1 - multiplier); ema.push(currentEMA); } else { ema.push(null); } } return ema; }; /** * Calculate Bollinger Bands */ const calculateBollingerBands = (data, period, standardDeviations = 2) => { if (data.length < period) return []; // Handle different input types const values = data.map(item => { if (typeof item === 'number') { return item; } else if (typeof item === 'object' && item !== null && 'close' in item) { return item.close; } else { return item.value; } }); const result = []; // Add null values for the first period-1 entries for (let i = 0; i < period - 1; i++) { result.push(null); } for (let i = period - 1; i < values.length; i++) { const periodValues = values.slice(i - period + 1, i + 1); const mean = periodValues.reduce((sum, val) => sum + val, 0) / period; // Calculate standard deviation const variance = periodValues.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / period; const stdDev = Math.sqrt(variance); result.push({ upper: mean + standardDeviations * stdDev, middle: mean, lower: mean - standardDeviations * stdDev }); } return result; }; /** * Calculate Relative Strength Index (RSI) */ const calculateRSI = (data, period = 14) => { if (data.length < period + 1) return []; // Handle different input types const values = data.map(item => { if (typeof item === 'number') { return item; } else if (typeof item === 'object' && item !== null && 'close' in item) { return item.close; } else { return item.value; } }); const rsi = []; // Add null values for the first period entries for (let i = 0; i < period; i++) { rsi.push(null); } // Calculate price changes const changes = []; for (let i = 1; i < values.length; i++) { changes.push(values[i] - values[i - 1]); } // Calculate initial average gains and losses let avgGain = 0; let avgLoss = 0; for (let i = 0; i < period; i++) { if (changes[i] > 0) { avgGain += changes[i]; } else { avgLoss += Math.abs(changes[i]); } } avgGain /= period; avgLoss /= period; // Calculate RSI for the first period if (avgLoss === 0) { rsi.push(avgGain === 0 ? 50 : 100); // Handle division by zero } else { const rs = avgGain / avgLoss; rsi.push(100 - 100 / (1 + rs)); } // Calculate RSI for remaining periods using smoothed averages for (let i = period; i < changes.length; i++) { const change = changes[i]; const gain = change > 0 ? change : 0; const loss = change < 0 ? Math.abs(change) : 0; avgGain = (avgGain * (period - 1) + gain) / period; avgLoss = (avgLoss * (period - 1) + loss) / period; if (avgLoss === 0) { rsi.push(avgGain === 0 ? 50 : 100); // Handle division by zero } else { const currentRS = avgGain / avgLoss; rsi.push(100 - 100 / (1 + currentRS)); } } return rsi; }; /** * Calculate MACD (Moving Average Convergence Divergence) */ const calculateMACD = (data, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) => { if (data.length === 0) { return { macd: [], signal: [], histogram: [] }; } const fastEMA = calculateEMA(data, fastPeriod); const slowEMA = calculateEMA(data, slowPeriod); // Initialize arrays with same length as input data const macd = new Array(data.length).fill(null); const signal = new Array(data.length).fill(null); const histogram = new Array(data.length).fill(null); // Calculate MACD line (can start when we have both EMAs) const macdStartIndex = Math.max(fastPeriod - 1, slowPeriod - 1); for (let i = macdStartIndex; i < data.length; i++) { const fastIndex = i - (fastPeriod - 1); const slowIndex = i - (slowPeriod - 1); if (fastIndex >= 0 && slowIndex >= 0 && fastIndex < fastEMA.length && slowIndex < slowEMA.length) { const fastValue = fastEMA[fastIndex]; const slowValue = slowEMA[slowIndex]; if (fastValue !== null && slowValue !== null) { macd[i] = fastValue - slowValue; } } } // Calculate signal line (EMA of MACD values) const macdValues = macd.filter((val, idx) => val !== null && idx >= macdStartIndex); if (macdValues.length >= signalPeriod) { const macdData = macdValues.map((value, index) => ({ time: index, value: value })); const signalEMA = calculateEMA(macdData, signalPeriod); // Place signal values in correct positions const signalStartIndex = macdStartIndex + signalPeriod - 1; for (let i = 0; i < signalEMA.length && signalStartIndex + i < data.length; i++) { signal[signalStartIndex + i] = signalEMA[i]; } } // Calculate histogram for (let i = 0; i < data.length; i++) { if (macd[i] !== null && signal[i] !== null) { histogram[i] = macd[i] - signal[i]; } } return { macd, signal, histogram }; }; /** * Calculate Volume Weighted Average Price (VWAP) */ const calculateVWAP = data => { if (data.length === 0) return []; const vwap = []; let cumulativeTPV = 0; // Typical Price * Volume let cumulativeVolume = 0; for (let i = 0; i < data.length; i++) { const candle = data[i]; const typicalPrice = (candle.high + candle.low + candle.close) / 3; const volume = candle.volume || 0; cumulativeTPV += typicalPrice * volume; cumulativeVolume += volume; if (cumulativeVolume > 0) { vwap.push(cumulativeTPV / cumulativeVolume); } else { vwap.push(typicalPrice); } } return vwap; }; /** * Calculate Average True Range (ATR) */ const calculateATR = (data, period = 14) => { if (data.length < period + 1) return []; const trueRanges = []; const result = []; // First value is always null result.push(null); // Calculate True Range for each period for (let i = 1; i < data.length; i++) { const current = data[i]; const previous = data[i - 1]; const tr1 = current.high - current.low; const tr2 = Math.abs(current.high - previous.close); const tr3 = Math.abs(current.low - previous.close); trueRanges.push(Math.max(tr1, tr2, tr3)); if (trueRanges.length < period) { result.push(null); } else if (trueRanges.length === period) { // First ATR is simple average const atr = trueRanges.reduce((sum, tr) => sum + tr, 0) / period; result.push(atr); } else { // Subsequent ATRs use smoothing const prevATR = result[result.length - 1]; const currentTR = trueRanges[trueRanges.length - 1]; const atr = (prevATR * (period - 1) + currentTR) / period; result.push(atr); } } return result; }; /** * Utility function to normalize indicator data to chart range */ const normalizeIndicatorData = (indicatorData, chartMin, chartMax, indicatorMin, indicatorMax) => { if (indicatorData.length === 0) return []; const dataMin = indicatorMin ?? Math.min(...indicatorData); const dataMax = indicatorMax ?? Math.max(...indicatorData); const dataRange = dataMax - dataMin; const chartRange = chartMax - chartMin; if (dataRange === 0) return indicatorData.map(() => chartMin); return indicatorData.map(value => { const normalizedValue = (value - dataMin) / dataRange; return chartMin + normalizedValue * chartRange; }); }; /** * Get indicator calculation function by type */ const getIndicatorCalculator = type => { switch (type) { case 'sma': return calculateSMA; case 'ema': return calculateEMA; case 'rsi': return calculateRSI; case 'vwap': return calculateVWAP; case 'atr': return calculateATR; default: throw new Error(`Unknown indicator type: ${type}`); } }; /** * Hook to calculate technical indicators for chart data */ const useIndicators = (data, configs) => { return react.useMemo(() => { if (!data || data.length === 0 || !configs || configs.length === 0) { return []; } return configs.map(config => { let indicatorData = []; try { switch (config.type) { case 'sma': if (config.period) { indicatorData = calculateSMA(data, config.period); } break; case 'ema': if (config.period) { const smoothing = 'smoothing' in config ? config.smoothing : 2; indicatorData = calculateEMA(data, config.period, smoothing); } break; case 'bollinger': if (config.period) { const standardDeviations = 'standardDeviations' in config ? config.standardDeviations : 2; const bands = calculateBollingerBands(data, config.period, standardDeviations); // For simplicity, return middle band (SMA). In a full implementation, // you might want to return all three bands indicatorData = bands.middle; } break; case 'volume': // Volume bars - return volume data if available if (data.length > 0 && 'volume' in data[0]) { indicatorData = data.map(candle => candle.volume || 0); } break; default: console.warn(`Unknown indicator type: ${config.type}`); } } catch (error) { console.error(`Error calculating ${config.type} indicator:`, error); } return { type: config.type, data: indicatorData, config }; }); }, [data, configs]); }; /** * Hook to calculate a single indicator */