UNPKG

candlestick-chart-generator

Version:

A Node.js library for generating candlestick chart screenshots using financial data

168 lines (143 loc) 6.06 kB
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;