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
JavaScript
'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
*/