@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
JavaScript
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';