Skip to main content

Overview

The Price Tracker Bot generates dynamic price history charts using Chart.js. Charts are rendered in a headless browser and sent as images via Telegram, providing users with visual insights into price trends over time.

How Chart Generation Works

Chart.js Integration

The bot uses Playwright to render Chart.js visualizations in a headless browser:
async function generateChartBuffer(labels, prices, title) {
  // Use Playwright to render a Chart.js chart and screenshot the canvas
  const browser = await chromium.launch({ headless: true });
  const ctx = await browser.newContext();
  const page = await ctx.newPage();

  const html = `
  <html>
    <head>
      <meta charset="utf-8" />
      <style>body{margin:0;padding:0}</style>
    </head>
    <body>
      <canvas id="chart" width="900" height="420"></canvas>
      <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
      <script>
        const labels = ${JSON.stringify(labels)};
        const data = ${JSON.stringify(prices)};
        const title = ${JSON.stringify(title || "")};
        const ctx = document.getElementById('chart').getContext('2d');
        new Chart(ctx, {
          type: 'line',
          data: {
            labels,
            datasets: [{
              label: 'Precio',
              data,
              borderWidth: 2,
              tension: 0.2,
              fill: false
            }]
          },
          options: {
            responsive: false,
            plugins: {
              title: { display: true, text: title }
            },
            scales: {
              y: { beginAtZero: false }
            }
          }
        });
      </script>
    </body>
  </html>
  `;

  await page.setContent(html, { waitUntil: "load" });
  // Wait a bit for Chart.js to render
  await page.waitForTimeout(1000);
  const canvas = await page.$("#chart");
  const buffer = await canvas.screenshot();
  await browser.close();
  return buffer;
}

Canvas Size

900x420 pixels for optimal mobile viewing

Chart Type

Line chart with smooth curves (tension: 0.2)

Y-Axis

Auto-scaled, not starting from zero

Render Time

1000ms wait for Chart.js to fully render

History Storage

Price history is stored as an array within each product object:
history: [
  { date: "2026-03-01T10:00:00.000Z", price: 34.99 },
  { date: "2026-03-01T12:00:00.000Z", price: 29.99 },
  { date: "2026-03-02T14:00:00.000Z", price: 27.99 }
]

History Limit

To prevent excessive data growth, history is limited to 120 entries:
const HISTORY_LIMIT = 120;

// Always append history
updated.history.push({ date: new Date().toISOString(), price: scraped.price });
if (updated.history.length > HISTORY_LIMIT) {
  updated.history = updated.history.slice(-HISTORY_LIMIT);
}
With checks every 2 hours, 120 entries represent approximately 10 days of price history.

Requesting a Chart

Users can request price charts using the /chart command:
bot.onText(/\/chart (.+)/, async (msg, match) => {
  const chatId = msg.chat.id;
  const raw = match[1].trim();
  const key = sanitizeAmazonURL(raw);

  if (!priceData[key]) {
    return bot.sendMessage(chatId, "⚠️ No encontré ese producto en seguimiento. Usa /list");
  }

  await sendPriceChart(chatId, key);
});

Example Usage

/chart https://www.amazon.com/dp/B08N5WRWNW

Chart Generation Process

The sendPriceChart() function (lines 309-333) handles the complete workflow:
async function sendPriceChart(chatId, productUrl) {
  const product = priceData[productUrl];
  if (!product) {
    return bot.sendMessage(chatId, "⚠️ Producto no encontrado en seguimiento. Usa /list");
  }

  if (!Array.isArray(product.history) || product.history.length < 2) {
    return bot.sendMessage(chatId, "📉 No hay suficiente historial para graficar (se requieren al menos 2 registros).");
  }

  const sorted = product.history
    .map(h => ({ date: new Date(h.date), price: h.price }))
    .sort((a, b) => a.date - b.date);

  const labels = sorted.map(s => 
    s.date.toLocaleString("es-MX", { 
      day: "2-digit", 
      month: "short", 
      hour: "2-digit", 
      minute: "2-digit" 
    })
  );
  const prices = sorted.map(s => s.price);

  try {
    const buf = await generateChartBuffer(labels, prices, product.title);
    await bot.sendPhoto(chatId, buf, {
      caption: `📊 *Histórico de precios*\n${escapeMD(product.title)}`,
      parse_mode: "Markdown",
      reply_markup: { 
        inline_keyboard: [[{ text: "🛒 Ver en Amazon", url: productUrl }]] 
      }
    });
  } catch (err) {
    console.error("❌ Error generando gráfico:", err.message);
    await bot.sendMessage(chatId, "❌ Ocurrió un error generando el gráfico.");
  }
}

Process Steps

Check if the product exists in the tracking database and has sufficient history (minimum 2 data points).
if (!product) {
  return bot.sendMessage(chatId, "⚠️ Producto no encontrado...");
}

if (!Array.isArray(product.history) || product.history.length < 2) {
  return bot.sendMessage(chatId, "📉 No hay suficiente historial...");
}
Sort price history chronologically to ensure proper chart ordering.
const sorted = product.history
  .map(h => ({ date: new Date(h.date), price: h.price }))
  .sort((a, b) => a.date - b.date);
Convert timestamps to human-readable date/time labels in Spanish (Mexico) format.
const labels = sorted.map(s => 
  s.date.toLocaleString("es-MX", { 
    day: "2-digit", 
    month: "short", 
    hour: "2-digit", 
    minute: "2-digit" 
  })
);
// Example: "01 mar, 10:00"
Create a parallel array of price values matching the labels.
const prices = sorted.map(s => s.price);
Call generateChartBuffer() to create the chart image buffer.
const buf = await generateChartBuffer(labels, prices, product.title);
Send the chart as a photo with caption and Amazon link button.
await bot.sendPhoto(chatId, buf, {
  caption: `📊 *Histórico de precios*\n${escapeMD(product.title)}`,
  parse_mode: "Markdown",
  reply_markup: { 
    inline_keyboard: [[{ text: "🛒 Ver en Amazon", url: productUrl }]] 
  }
});

Interactive Chart Access

Charts can also be accessed via inline buttons in the daily summary and product details:
// In product detail view
const keyboard = {
  inline_keyboard: [
    [{ text: "🛒 Ver en Amazon", url: product.url }],
    [
      { text: "✍🏻 Editar URL", callback_data: `edit_product:${productUrl}` },
      { text: "🗑️ Eliminar", callback_data: `delete_product:${productUrl}` }
    ],
    [{ text: "⏪ Volver a la lista", callback_data: "list" }],
    [{ text: "📈 Ver gráfico", callback_data: `chart:${productUrl}` }]
  ]
};

Callback Handler

} else if (data.startsWith("chart:")) {
  const url = data.substring("chart:".length);
  await sendPriceChart(chatId, url);
}
Users can quickly view charts without typing URLs by using the inline buttons.

Chart Customization

The chart configuration uses these settings:
SettingValuePurpose
type'line'Line chart for trend visualization
borderWidth2Line thickness for visibility
tension0.2Slight curve for smooth appearance
fillfalseNo area fill under the line
responsivefalseFixed dimensions for consistency
beginAtZerofalseY-axis scales to data range

Error Handling

If chart generation fails, users receive a friendly error message:
} catch (err) {
  console.error("❌ Error generando gráfico:", err.message);
  await bot.sendMessage(chatId, "❌ Ocurrió un error generando el gráfico.");
}
Common errors include insufficient history, browser launch failures, or Chart.js rendering issues.

Performance Considerations

  • Browser lifecycle: Each chart generation launches and closes a dedicated browser instance
  • Render wait: 1000ms delay ensures Chart.js completes rendering before screenshot
  • Memory usage: Buffers are sent immediately and not stored on disk
  • CDN dependency: Requires internet access to load Chart.js from cdn.jsdelivr.net

Example Chart Output

When a user requests a chart, they receive an image showing:
  • X-axis: Timestamps (e.g., “10 mar, 14:00”)
  • Y-axis: Price values (auto-scaled)
  • Title: Product name
  • Line: Price trend over time with smooth curves
  • Caption: ”📊 Histórico de precios” + product title
  • Button: ”🛒 Ver en Amazon” link
This provides a clear visual representation of price fluctuations, helping users identify trends and optimal purchase times.

Build docs developers (and LLMs) love