@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.
191 lines (190 loc) • 7.51 kB
JavaScript
import { ChartConfig } from '../core/config.js';
import { ChartRenderer } from '../core/renderer.js';
import { ComparisonRenderer } from '../renderer/comparison.js';
import { DataProvider } from '../utils/provider.js';
export class ComparisonService {
static DEFAULT_LAYOUT_TYPE = 'side-by-side';
config;
dataProvider;
constructor(config) {
this.config = config;
this.dataProvider = new DataProvider({
name: (config.exchange || 'binance'),
sandbox: false
});
}
async generateComparison() {
try {
const { symbols, layout = { type: ComparisonService.DEFAULT_LAYOUT_TYPE }, width = 1600, height = 800 } = this.config;
const comparisonRenderer = new ComparisonRenderer(width, height, layout);
await this.addChartsToRenderer(comparisonRenderer, symbols);
await comparisonRenderer.renderComparison();
const buffer = await comparisonRenderer.exportComparison({ format: 'png' });
return this.createSuccessResult(buffer);
}
catch (error) {
return this.createErrorResult(error);
}
}
async addChartsToRenderer(comparisonRenderer, symbols) {
if (this.config.timeframes && this.config.timeframes.length > 0) {
await this.addTimeframeCharts(comparisonRenderer, symbols);
}
else {
await this.addSymbolCharts(comparisonRenderer, symbols);
}
}
async addTimeframeCharts(comparisonRenderer, symbols) {
const symbol = symbols[0];
const maxCharts = Math.min(symbols.length, this.config.timeframes.length);
for (let i = 0; i < maxCharts; i++) {
const timeframe = this.config.timeframes[i];
const chartData = await this.generateChartData(symbol, timeframe);
if (chartData) {
comparisonRenderer.addChart(chartData);
}
}
}
async addSymbolCharts(comparisonRenderer, symbols) {
for (const symbol of symbols) {
const chartData = await this.generateChartData(symbol);
if (chartData) {
comparisonRenderer.addChart(chartData);
}
}
}
async createSuccessResult(buffer) {
if (this.config.outputPath) {
const fs = await import('fs/promises');
await fs.writeFile(this.config.outputPath, buffer);
return {
success: true,
outputPath: this.config.outputPath,
buffer
};
}
return {
success: true,
buffer
};
}
createErrorResult(error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
async generateChartData(symbol, timeframe) {
try {
const chartConfig = new ChartConfig({
symbol,
timeframe: timeframe || this.config.timeframe || '1h',
exchange: this.config.exchange || 'binance',
width: 800,
height: 600,
theme: this.config.theme || 'dark',
chartType: this.config.chartType || 'candlestick',
showTitle: true,
showTimeAxis: true,
showGrid: true
});
const renderer = new ChartRenderer(chartConfig);
const result = await renderer.generateChart();
if (result.success) {
const data = await this.dataProvider.fetchOHLCV(symbol, timeframe || this.config.timeframe || '1h', 100);
const chartData = this.createChartData(data, chartConfig);
return chartData;
}
return null;
}
catch (error) {
console.warn(`Failed to generate chart for ${symbol}:`, error);
return null;
}
}
createChartData(data, config) {
const ohlc = data.map(item => ({
time: item.timestamp,
open: item.open,
high: item.high,
low: item.low,
close: item.close,
...(item.volume !== undefined && { volume: item.volume })
}));
return {
ohlc,
levels: [],
config: {
width: config.width,
height: config.height,
backgroundColor: config.backgroundColor || '#1e222d',
textColor: config.textColor || '#ffffff',
gridColor: '#2b2b43',
borderColor: '#2b2b43',
chartType: config.chartType || 'candlestick',
customBarColors: {
bullish: this.config.customBarColors?.bullish || '#26a69a',
bearish: this.config.customBarColors?.bearish || '#ef5350',
wick: this.config.customBarColors?.wick || '#424242',
border: this.config.customBarColors?.border || '#E0E0E0'
},
horizontalLevels: [],
title: `${config.symbol} ${config.timeframe}`,
showTitle: true,
showTimeAxis: true,
showGrid: true,
showVWAP: this.config.showVWAP === true,
showEMA: this.config.showEMA === true,
...(this.config.emaPeriod !== undefined && { emaPeriod: this.config.emaPeriod }),
showSMA: this.config.showSMA === true,
...(this.config.smaPeriod !== undefined && { smaPeriod: this.config.smaPeriod }),
showBollingerBands: this.config.showBollingerBands === true,
...(this.config.bbPeriod !== undefined && { bbPeriod: this.config.bbPeriod }),
...(this.config.bbStandardDeviations !== undefined && {
bbStandardDeviations: this.config.bbStandardDeviations
}),
...(this.config.bbColors !== undefined && { bbColors: this.config.bbColors })
}
};
}
static async sideBySide(symbols, outputPath, config) {
const service = new ComparisonService({
symbols,
outputPath,
layout: { type: ComparisonService.DEFAULT_LAYOUT_TYPE, gap: 20 },
...config
});
return service.generateComparison();
}
static async grid(symbols, columns, outputPath, config) {
if (symbols.length > 2) {
return {
success: false,
error: `Grid layout supports maximum 2 symbols. Got ${symbols.length} symbols: ${symbols.join(', ')}`
};
}
if (columns > 2) {
return {
success: false,
error: `Grid layout supports maximum 2 columns. Got ${columns} columns`
};
}
const service = new ComparisonService({
symbols,
outputPath,
layout: { type: 'grid', columns, gap: 15 },
...config
});
return service.generateComparison();
}
static async timeframeComparison(symbol, timeframes, outputPath, config) {
const service = new ComparisonService({
symbols: [symbol],
timeframes,
outputPath,
layout: { type: ComparisonService.DEFAULT_LAYOUT_TYPE, gap: 20 },
...config
});
return service.generateComparison();
}
}