UNPKG

@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.

142 lines (141 loc) 6.14 kB
import { createCanvas, Canvas, CanvasRenderingContext2D } from 'canvas'; import { CandlestickRenderer, LineRenderer, AreaRenderer, HeikinAshiRenderer, RenkoRenderer, LineBreakRenderer } from '../renderer/charts.js'; import { AxesRenderer, GridRenderer, VWAPRenderer, EMARenderer, SMARenderer, BollingerBandsRenderer, LevelsRenderer, TitleRenderer, WatermarkRenderer } from '../renderer/elements.js'; import { calculatePriceRange, hasVolumeData } from '../renderer/utils.js'; export class NodeChartRenderer { canvas; ctx; width; height; chartData; constructor(width, height) { this.width = width; this.height = height; this.canvas = createCanvas(width, height); this.ctx = this.canvas.getContext('2d'); } async renderChart(data) { this.chartData = data; this.ctx.clearRect(0, 0, this.width, this.height); this.ctx.fillStyle = data.config.backgroundColor || '#1e222d'; this.ctx.fillRect(0, 0, this.width, this.height); await this.drawChart(); if (data.config.showTitle !== false && data.config.title) { const titleRenderer = new TitleRenderer(this.ctx, this.width, data.config); titleRenderer.render(data.config.title); } if (data.config.watermark) { const watermarkRenderer = new WatermarkRenderer(this.ctx, this.width, this.height); watermarkRenderer.render(data.config.watermark); } } async drawChart() { if (!this.chartData) return; const { ohlc, config } = this.chartData; if (ohlc.length === 0) return; const margin = config.margin || { top: 60, bottom: 40, left: 60, right: 40 }; const chartWidth = this.width - (margin.left || 0) - (margin.right || 0); const chartHeight = this.height - (margin.top || 0) - (margin.bottom || 0); const dimensions = { width: this.width, height: this.height, margin: { top: margin.top || 0, bottom: margin.bottom || 0, left: margin.left || 0, right: margin.right || 0 }, chartWidth, chartHeight }; const priceRange = calculatePriceRange(ohlc, config); if (config.showBollingerBands && config.bbPeriod) { const bbRenderer = new BollingerBandsRenderer(this.ctx, dimensions, priceRange, config, config.bbPeriod, config.bbStandardDeviations || 2); bbRenderer.renderBackground(ohlc); } this.drawChartType(ohlc, dimensions, priceRange, config); if (config.showVWAP && hasVolumeData(ohlc)) { const vwapRenderer = new VWAPRenderer(this.ctx, dimensions, priceRange, config); vwapRenderer.render(ohlc); } if (config.showEMA && config.emaPeriod) { const emaRenderer = new EMARenderer(this.ctx, dimensions, priceRange, config, config.emaPeriod); emaRenderer.render(ohlc); } if (config.showSMA && config.smaPeriod) { const smaRenderer = new SMARenderer(this.ctx, dimensions, priceRange, config, config.smaPeriod); smaRenderer.render(ohlc); } if (config.showBollingerBands && config.bbPeriod) { const bbRenderer = new BollingerBandsRenderer(this.ctx, dimensions, priceRange, config, config.bbPeriod, config.bbStandardDeviations || 2); bbRenderer.renderLines(ohlc); } if (this.chartData.levels) { const levelsRenderer = new LevelsRenderer(this.ctx, dimensions, priceRange); levelsRenderer.render(this.chartData.levels); } if (config.showGrid !== false) { const gridRenderer = new GridRenderer(this.ctx, dimensions, config); gridRenderer.render(); } const axesRenderer = new AxesRenderer(this.ctx, dimensions, priceRange, config); axesRenderer.render(ohlc); } drawChartType(ohlc, dimensions, priceRange, config) { switch (config.chartType || 'candlestick') { case 'candlestick': { const candlestickRenderer = new CandlestickRenderer(this.ctx, dimensions, priceRange, config); candlestickRenderer.render(ohlc); break; } case 'line': { const lineRenderer = new LineRenderer(this.ctx, dimensions, priceRange, config); lineRenderer.render(ohlc); break; } case 'area': { const areaRenderer = new AreaRenderer(this.ctx, dimensions, priceRange, config); areaRenderer.render(ohlc); break; } case 'heikin-ashi': { const heikinAshiRenderer = new HeikinAshiRenderer(this.ctx, dimensions, priceRange, config); heikinAshiRenderer.render(ohlc); break; } case 'renko': { const renkoRenderer = new RenkoRenderer(this.ctx, dimensions, priceRange, config); renkoRenderer.render(ohlc); break; } case 'line-break': { const lineBreakRenderer = new LineBreakRenderer(this.ctx, dimensions, priceRange, config); lineBreakRenderer.render(ohlc); break; } } } async exportChart(options) { const format = options.format === 'jpg' ? 'jpeg' : options.format; if (format === 'png') { return this.canvas.toBuffer('image/png'); } else { return this.canvas.toBuffer('image/jpeg', { quality: options.quality || 0.9 }); } } async saveChart(outputPath, options) { const fs = await import('fs/promises'); const buffer = await this.exportChart(options); await fs.writeFile(outputPath, buffer); } chartElement() { return this.canvas; } } export * from './types.js'; export * from './utils.js'; export * from './charts.js'; export * from './elements.js';