Chart.js Lato Server? Perché?
Ogni tanto serve generare grafici lato server: email con report, PDF, dashboard embeddabili, thumbnail social.
Opzioni classiche:
- Puppeteer (pesante, cold start lenti)
- node-canvas (limitato, plugin problematici)
- 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, radardata: valori comma-separatedlabels: 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)
- Native Rendering: Replace proxy con
chartjs-node-canvas - Packagist Publish: Package installabile via Composer
- Advanced Charts: Annotation, datalabels plugins
- Laravel Helper: Global
chart()helper oltre al Facade - 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.