candlestick-chart-generator
Version:
A Node.js library for generating candlestick chart screenshots using financial data
168 lines (143 loc) • 6.06 kB
JavaScript
const { createCanvas } = require('canvas');
class ChartRendererSimple {
constructor(width = 1200, height = 600) {
this.width = width;
this.height = height;
}
/**
* Render a candlestick chart using pure Canvas API
* @param {Array} data - Chart data in OHLC format
* @param {Object} options - Chart rendering options
* @param {string} [options.symbol] - The symbol to display on the chart
* @param {string} [options.timeframe] - The timeframe to display on the chart
* @param {Object} [options.chartOptions] - Additional chart rendering options
* @returns {Promise<Buffer>} Buffer containing the chart image
*/
async renderChart(data, options = {}) {
const { chartOptions = {}, symbol, timeframe } = options;
// Create canvas
const canvas = createCanvas(this.width, this.height);
const ctx = canvas.getContext('2d');
// Set background
const bgColor = chartOptions.backgroundColor || '#131722';
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, this.width, this.height);
// Draw symbol and timeframe
if (symbol || timeframe) {
ctx.fillStyle = '#d1d4dc';
ctx.font = 'bold 20px Arial';
ctx.textAlign = 'left';
let textX = 20;
let textY = 30;
if (symbol) {
ctx.fillText(symbol, textX, textY);
textY += 25;
}
if (timeframe) {
ctx.font = '16px Arial';
ctx.fillText(`Timeframe: ${timeframe}`, textX, textY);
}
}
// Calculate chart area (leave margins for labels)
const margin = { top: 80, right: 80, bottom: 60, left: 80 };
const chartWidth = this.width - margin.left - margin.right;
const chartHeight = this.height - margin.top - margin.bottom;
// Find min/max values for scaling
const prices = data.flatMap(d => [d.open, d.high, d.low, d.close]);
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
const priceRange = maxPrice - minPrice;
const padding = priceRange * 0.1; // 10% padding
// Scale functions
const xScale = (index) => margin.left + (index / (data.length - 1)) * chartWidth;
const yScale = (price) => margin.top + ((maxPrice + padding - price) / (priceRange + 2 * padding)) * chartHeight;
// Draw grid lines
ctx.strokeStyle = 'rgba(42, 46, 57, 0.5)';
ctx.lineWidth = 1;
// Horizontal grid lines
for (let i = 0; i <= 5; i++) {
const y = margin.top + (i / 5) * chartHeight;
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + chartWidth, y);
ctx.stroke();
}
// Vertical grid lines
for (let i = 0; i <= 10; i++) {
const x = margin.left + (i / 10) * chartWidth;
ctx.beginPath();
ctx.moveTo(x, margin.top);
ctx.lineTo(x, margin.top + chartHeight);
ctx.stroke();
}
// Draw candlesticks
const candleWidth = Math.max(2, chartWidth / data.length * 0.6);
data.forEach((d, index) => {
const x = xScale(index);
const openY = yScale(d.open);
const closeY = yScale(d.close);
const highY = yScale(d.high);
const lowY = yScale(d.low);
// Determine color (green for up, red for down)
const isUp = d.close >= d.open;
const color = isUp ? '#26a69a' : '#ef5350';
// Draw wick (high-low line)
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, highY);
ctx.lineTo(x, lowY);
ctx.stroke();
// Draw body (open-close rectangle)
ctx.fillStyle = color;
const bodyTop = Math.min(openY, closeY);
const bodyHeight = Math.abs(closeY - openY);
if (bodyHeight > 0) {
ctx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight);
} else {
// Doji - draw a line
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x - candleWidth / 2, openY);
ctx.lineTo(x + candleWidth / 2, openY);
ctx.stroke();
}
});
// Draw price labels
ctx.fillStyle = '#d1d4dc';
ctx.font = '12px Arial';
ctx.textAlign = 'left';
for (let i = 0; i <= 5; i++) {
const price = maxPrice + padding - (i / 5) * (priceRange + 2 * padding);
const y = margin.top + (i / 5) * chartHeight;
ctx.fillText(price.toFixed(2), margin.left + chartWidth + 10, y + 4);
}
// Draw time labels (simplified)
ctx.textAlign = 'center';
const labelIndices = [0, Math.floor(data.length / 4), Math.floor(data.length / 2), Math.floor(3 * data.length / 4), data.length - 1];
labelIndices.forEach(index => {
if (index < data.length) {
const x = xScale(index);
const date = new Date(data[index].time * 1000);
const label = date.toLocaleDateString();
ctx.fillText(label, x, margin.top + chartHeight + 20);
}
});
// Draw border
ctx.strokeStyle = '#d1d4dc';
ctx.lineWidth = 1;
ctx.strokeRect(margin.left, margin.top, chartWidth, chartHeight);
return canvas.toBuffer('image/png');
}
/**
* Set the width and height for the chart
* @param {number} width - Chart width in pixels
* @param {number} height - Chart height in pixels
*/
setWidthAndHeight(width, height) {
this.width = width;
this.height = height;
}
}
module.exports = ChartRendererSimple;