QuickChart: Server-side Chart.js con Cloudflare Workers e Laravel Package

Nerd Level: 3/5
QuickChart: Server-side Chart.js con Cloudflare Workers e Laravel Package

Chart.js Lato Server? Perché?

Ogni tanto serve generare grafici lato server: email con report, PDF, dashboard embeddabili, thumbnail social.

Opzioni classiche:

  1. Puppeteer (pesante, cold start lenti)
  2. node-canvas (limitato, plugin problematici)
  3. Servizio esterno tipo QuickChart.io (vendor lock-in)

Idea: Cloniamo QuickChart con Cloudflare Workers. Zero-ops, edge caching globale, free tier generoso.


QuickChart: La Soluzione

Live demo: https://quickchart.giobi.com

Tech stack:

  • Cloudflare Workers (TypeScript) - Runtime edge
  • KV Storage - Cache distribuita 10 giorni
  • Laravel Package (PHP) - Fluent API wrapper
  • Bootstrap 5 - Demo homepage

Performance:

  • Cold start: <50ms
  • Cache HIT: ~30ms
  • Cache MISS: ~450ms (rendering + cache write)
  • Free tier: 100k requests/day = 3M/mese

API Usage: GET Semplice

# Bar chart
https://quickchart.giobi.com/chart?type=bar&data=10,20,30,25&labels=Q1,Q2,Q3,Q4&title=Sales

# Line chart con colori custom
https://quickchart.giobi.com/chart?type=line&data=5,10,15,20&labels=Jan,Feb,Mar,Apr&backgroundColor=rgba(75,192,192,0.8)&borderColor=rgba(75,192,192,1)

# Pie chart
https://quickchart.giobi.com/chart?type=pie&data=30,50,20&labels=Red,Blue,Yellow&width=500&height=500

Parametri disponibili:

  • type: bar, line, pie, doughnut, radar
  • data: valori comma-separated
  • labels: etichette comma-separated (optional)
  • width / height: dimensioni (default 800x400, max 2000x2000)
  • title: titolo chart (optional)
  • backgroundColor / borderColor: colori RGBA (optional)

Output: PNG image, cachata globalmente.


Laravel Package: Fluent API

composer require giobi/quickchart-laravel

Usage

use Giobi\QuickChart\Facades\Chart;

// Basic bar chart
{!! Chart::bar([10, 20, 30, 25])
    ->labels(['Q1', 'Q2', 'Q3', 'Q4'])
    ->title('Quarterly Sales')
    ->render() !!}

// Line chart styled
{!! Chart::line([100, 150, 120, 200])
    ->labels(['Jan', 'Feb', 'Mar', 'Apr'])
    ->title('Revenue 2024')
    ->size(800, 400)
    ->backgroundColor('rgba(54, 162, 235, 0.8)')
    ->borderColor('rgba(54, 162, 235, 1)')
    ->render(['class' => 'img-fluid', 'loading' => 'lazy']) !!}

// Pie chart
{!! Chart::pie([30, 50, 20])
    ->labels(['Product A', 'Product B', 'Product C'])
    ->title('Market Share')
    ->size(500, 500)
    ->render() !!}

Controller Example

namespace App\Http\Controllers;

use Giobi\QuickChart\Facades\Chart;

class DashboardController extends Controller
{
    public function index()
    {
        $salesChart = Chart::bar([100, 150, 120, 200])
            ->labels(['Q1', 'Q2', 'Q3', 'Q4'])
            ->title('Quarterly Sales')
            ->backgroundColor('rgba(54, 162, 235, 0.8)');

        $trafficChart = Chart::line([1200, 1900, 3000, 5000])
            ->labels(['Week 1', 'Week 2', 'Week 3', 'Week 4'])
            ->title('Weekly Traffic')
            ->backgroundColor('rgba(75, 192, 192, 0.8)');

        return view('dashboard', compact('salesChart', 'trafficChart'));
    }
}

Blade Template

<div class="row">
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">Sales</div>
            <div class="card-body">
                {!! $salesChart->render(['class' => 'img-fluid']) !!}
            </div>
        </div>
    </div>
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">Traffic</div>
            <div class="card-body">
                {!! $trafficChart->render() !!}
            </div>
        </div>
    </div>
</div>

Architettura Worker

TypeScript Implementation

export interface Env {
  CHART_CACHE?: KVNamespace;
  CACHE_TTL: string;
  MAX_WIDTH: string;
  MAX_HEIGHT: string;
}

async function handleChartRequest(url: URL, env: Env): Promise<Response> {
  // Parse config
  const config = parseChartConfig(url, env);

  // Generate cache key (hash of config)
  const cacheKey = generateCacheKey(config);

  // Try cache (KV)
  if (env.CHART_CACHE) {
    const cached = await env.CHART_CACHE.get(cacheKey, 'arrayBuffer');
    if (cached) {
      return new Response(cached, {
        headers: { 'X-Cache': 'HIT', 'Content-Type': 'image/png' }
      });
    }
  }

  // Render chart (currently proxies to quickchart.io)
  const imageBuffer = await renderChart(config);

  // Store in cache (10 days TTL)
  if (env.CHART_CACHE && imageBuffer) {
    await env.CHART_CACHE.put(cacheKey, imageBuffer, {
      expirationTtl: parseInt(env.CACHE_TTL) // 864000 = 10 days
    });
  }

  return new Response(imageBuffer, {
    headers: { 'X-Cache': 'MISS', 'Content-Type': 'image/png' }
  });
}

Caching Strategy

Hash-based key: Stesso config = stesso chart = stesso cache

function generateCacheKey(config: ChartConfig): string {
  const configStr = JSON.stringify(config);
  return `chart:${hashString(configStr)}`;
}

Verifica cache:

curl -I "https://quickchart.giobi.com/chart?type=bar&data=10,20,30"
# X-Cache: MISS (prima richiesta)

curl -I "https://quickchart.giobi.com/chart?type=bar&data=10,20,30"
# X-Cache: HIT (richiesta successiva, ~30ms)

Deployment: Cloudflare Workers

Setup

npm install wrangler --save-dev
npx wrangler init

wrangler.toml

name = "quickchart"
main = "src/index.ts"
compatibility_date = "2024-11-19"
account_id = "YOUR_ACCOUNT_ID"

[[kv_namespaces]]
binding = "CHART_CACHE"
id = "YOUR_KV_NAMESPACE_ID"

[vars]
CACHE_TTL = "864000"  # 10 days
MAX_WIDTH = "2000"
MAX_HEIGHT = "2000"

Deploy

export CLOUDFLARE_API_TOKEN="your_token"
npx wrangler deploy

Risultato: Live in <10 secondi su edge globale.


Custom Domain

DNS Setup

Cloudflare DNS record:

  • Type: AAAA
  • Name: quickchart.yourdomain.com
  • Content: 100:: (Workers placeholder)
  • Proxied: Yes

Worker Route

Cloudflare Dashboard → Workers → quickchart → Settings → Triggers:

  • Route: quickchart.yourdomain.com/*
  • Zone: yourdomain.com

Done: https://quickchart.yourdomain.com live.


Performance Metrics

Bundle Size

Total Upload: 28.16 KiB / gzip: 6.11 KiB

Small enough per edge deployment, include Bootstrap demo homepage.

Response Times

Scenario Time Location
Cold start <50ms Global edge
Cache HIT ~30ms Nearest POP
Cache MISS ~450ms Render + cache

Cache Efficiency

Con TTL 10 giorni + hash-based keys:

  • Cache hit ratio: >80% (chart statici)
  • Bandwidth saved: ~95% (edge-cached PNG)
  • Origin calls: Minimizzati

Color Palette Reference

// Blue
Chart::bar($data)
    ->backgroundColor('rgba(54, 162, 235, 0.8)')
    ->borderColor('rgba(54, 162, 235, 1)');

// Green
Chart::bar($data)
    ->backgroundColor('rgba(75, 192, 192, 0.8)')
    ->borderColor('rgba(75, 192, 192, 1)');

// Red
Chart::bar($data)
    ->backgroundColor('rgba(255, 99, 132, 0.8)')
    ->borderColor('rgba(255, 99, 132, 1)');

// Yellow
Chart::bar($data)
    ->backgroundColor('rgba(255, 206, 86, 0.8)')
    ->borderColor('rgba(255, 206, 86, 1)');

// Purple
Chart::bar($data)
    ->backgroundColor('rgba(153, 102, 255, 0.8)')
    ->borderColor('rgba(153, 102, 255, 1)');

// Orange
Chart::bar($data)
    ->backgroundColor('rgba(255, 159, 64, 0.8)')
    ->borderColor('rgba(255, 159, 64, 1)');

Lessons Learned

1. Cloudflare Workers = Zero-Ops Reale

Deploy in <10 secondi, niente server da gestire, scale automatico, free tier abbondante. La miglior esperienza serverless mai provata.

2. KV Cache = Game Changer

10 giorni di TTL per chart statici = cache hit ratio altissimo. Response time da 450ms a 30ms, bandwidth risparmiata ~95%.

3. Fluent API > Config Array

// Brutto
chart(['type' => 'bar', 'data' => [10,20,30], 'labels' => ['A','B','C']]);

// Bello
Chart::bar([10,20,30])->labels(['A','B','C'])->render();

Laravel developers apprezzano method chaining, autocomplete, type hints.

4. Bootstrap Demo > API Docs

Homepage con visual examples + code samples > simple API reference. Utenti vedono immediatamente cosa possono fare.

5. Custom Domain = Professionalità

quickchart.giobi.com > quickchart.giobi.workers.dev

Setup rapido con Cloudflare DNS, zero downtime.


Next Steps (Optional)

  1. Native Rendering: Replace proxy con chartjs-node-canvas
  2. Packagist Publish: Package installabile via Composer
  3. Advanced Charts: Annotation, datalabels plugins
  4. Laravel Helper: Global chart() helper oltre al Facade
  5. GitHub Pages: Deploy demo su gh-pages

Conclusione

Da zero a production in 4 ore:

  • ✅ Cloudflare Workers deployment
  • ✅ KV caching 10 giorni
  • ✅ Laravel package con fluent API
  • ✅ Bootstrap demo homepage
  • ✅ Custom domain configurato

Costi: €0/mese (free tier 100k req/day)

Performance: 30ms cache hit, 450ms cache miss

Developer Experience: Fluent API, auto-discovery, zero config

Links:


P.S. Il rendering attualmente usa proxy a quickchart.io (POC phase). Native rendering con chartjs-node-canvas è next step, ma per 95% use cases proxy + cache è più che sufficiente.