@neabyte/chart-to-image
Version:
Convert trading charts to images using Node.js canvas with advanced features: 6 chart types, VWAP/EMA/SMA indicators, custom colors, themes, hide elements, scaling, and PNG/JPEG export formats.
129 lines (128 loc) • 4 kB
JavaScript
export function formatTimestamp(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
if (isToday) {
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
else {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
}
export function calculatePercentageChange(current, previous) {
if (previous === 0)
return 0;
return ((current - previous) / previous) * 100;
}
export function calculateMovingAverage(data, period) {
if (data.length < period)
return [];
const result = [];
for (let i = period - 1; i < data.length; i++) {
const sum = data.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
result.push(sum / period);
}
return result;
}
export function calculateRSI(data, period = 14) {
if (data.length < period + 1)
return [];
const rsiValues = [];
for (let i = period; i < data.length; i++) {
const rsi = calculateRSIForPeriod(data, i, period);
rsiValues.push(rsi);
}
return rsiValues;
}
function calculateRSIForPeriod(data, index, period) {
let gains = 0;
let losses = 0;
for (let i = index - period + 1; i <= index; i++) {
const change = data[i] - data[i - 1];
if (change > 0) {
gains += change;
}
else {
losses -= change;
}
}
const avgGain = gains / period;
const avgLoss = losses / period;
if (avgLoss === 0)
return 100;
const rs = avgGain / avgLoss;
return 100 - 100 / (1 + rs);
}
export function validateChartData(data) {
if (!Array.isArray(data) || data.length === 0)
return false;
return data.every(item => {
return (typeof item.timestamp === 'number' &&
typeof item.open === 'number' &&
typeof item.high === 'number' &&
typeof item.low === 'number' &&
typeof item.close === 'number' &&
item.high >= Math.max(item.open, item.close) &&
item.low <= Math.min(item.open, item.close) &&
item.timestamp > 0);
});
}
export function sortChartData(data) {
return [...data].sort((a, b) => a.timestamp - b.timestamp);
}
export function filterChartDataByDate(data, startDate, endDate) {
const startTimestamp = startDate.getTime();
const endTimestamp = endDate.getTime();
return data.filter(item => {
return item.timestamp >= startTimestamp && item.timestamp <= endTimestamp;
});
}
export function timeframeToMs(timeframe) {
const value = parseInt(timeframe.slice(0, -1));
const unit = timeframe.slice(-1);
switch (unit) {
case 'm':
return value * 60 * 1000;
case 'h':
return value * 60 * 60 * 1000;
case 'd':
return value * 24 * 60 * 60 * 1000;
case 'w':
return value * 7 * 24 * 60 * 60 * 1000;
default:
throw new Error(`Unsupported timeframe unit: ${unit}`);
}
}
export function getPriceChangeColor(current, previous) {
if (current > previous)
return '#26a69a';
if (current < previous)
return '#ef5350';
return '#424242';
}
export function formatPrice(price, decimals = 2) {
return price.toFixed(decimals);
}
export function calculateVWAP(data) {
if (data.length === 0)
return 0;
let totalVolume = 0;
let totalVolumePrice = 0;
data.forEach(item => {
const volume = item.volume || 0;
const typicalPrice = (item.high + item.low + item.close) / 3;
totalVolume += volume;
totalVolumePrice += volume * typicalPrice;
});
return totalVolume > 0 ? totalVolumePrice / totalVolume : 0;
}