Buckets:

immErfanrajabee/bd / chart-engine.ts
imerfanrajabee's picture
download
raw
14 kB
export type ChartType = 'bar' | 'line' | 'pie' | 'doughnut' | 'area' | 'horizontalBar';
export interface ChartDataPoint {
label: string;
value: number;
color?: string;
}
export interface ChartConfig {
type: ChartType;
title: string;
data: ChartDataPoint[];
width?: number;
height?: number;
showLegend?: boolean;
showGrid?: boolean;
yAxisLabel?: string;
xAxisLabel?: string;
}
const DEFAULT_COLORS = [
'#4f8cf7', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6',
'#ec4899', '#06b6d4', '#f97316', '#14b8a6', '#a855f7',
];
function getColor(index: number): string {
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
}
function drawRoundedRect(
ctx: CanvasRenderingContext2D,
x: number, y: number, w: number, h: number, r: number,
) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function renderBarChart(ctx: CanvasRenderingContext2D, config: ChartConfig, w: number, h: number) {
const pad = { top: 60, right: 30, bottom: 60, left: 60 };
const chartW = w - pad.left - pad.right;
const chartH = h - pad.top - pad.bottom;
const maxVal = Math.max(...config.data.map(d => d.value), 1);
const barWidth = Math.min(chartW / config.data.length * 0.6, 60);
const gap = chartW / config.data.length;
ctx.fillStyle = 'rgba(255,255,255,0.04)';
drawRoundedRect(ctx, pad.left, pad.top, chartW, chartH, 12);
ctx.fill();
if (config.showGrid) {
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = pad.top + (chartH / 4) * i;
ctx.beginPath();
ctx.moveTo(pad.left, y);
ctx.lineTo(pad.left + chartW, y);
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = '11px Tahoma, sans-serif';
ctx.textAlign = 'right';
ctx.fillText(String(Math.round(maxVal - (maxVal / 4) * i)), pad.left - 8, y + 4);
}
}
config.data.forEach((point, i) => {
const x = pad.left + i * gap + (gap - barWidth) / 2;
const barH = (point.value / maxVal) * chartH;
const y = pad.top + chartH - barH;
const color = point.color || getColor(i);
const grad = ctx.createLinearGradient(x, y, x, pad.top + chartH);
grad.addColorStop(0, color);
grad.addColorStop(1, color + '40');
ctx.fillStyle = grad;
drawRoundedRect(ctx, x, y, barWidth, barH, 4);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '11px Tahoma, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.label, x + barWidth / 2, pad.top + chartH + 18);
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = 'bold 12px Tahoma, sans-serif';
ctx.fillText(formatNumber(point.value), x + barWidth / 2, y - 6);
});
}
function renderHorizontalBarChart(ctx: CanvasRenderingContext2D, config: ChartConfig, w: number, h: number) {
const pad = { top: 60, right: 30, bottom: 30, left: 100 };
const chartW = w - pad.left - pad.right;
const chartH = h - pad.top - pad.bottom;
const maxVal = Math.max(...config.data.map(d => d.value), 1);
const barHeight = Math.min(chartH / config.data.length * 0.6, 35);
const gap = chartH / config.data.length;
config.data.forEach((point, i) => {
const y = pad.top + i * gap + (gap - barHeight) / 2;
const barW = (point.value / maxVal) * chartW;
const color = point.color || getColor(i);
const grad = ctx.createLinearGradient(pad.left, y, pad.left + chartW, y);
grad.addColorStop(0, color + '80');
grad.addColorStop(1, color);
ctx.fillStyle = grad;
drawRoundedRect(ctx, pad.left, y, barW, barHeight, 4);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '12px Tahoma, sans-serif';
ctx.textAlign = 'right';
ctx.fillText(point.label, pad.left - 10, y + barHeight / 2 + 4);
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = 'bold 12px Tahoma, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(formatNumber(point.value), pad.left + barW + 8, y + barHeight / 2 + 4);
});
}
function renderLineChart(ctx: CanvasRenderingContext2D, config: ChartConfig, w: number, h: number) {
const pad = { top: 60, right: 30, bottom: 60, left: 60 };
const chartW = w - pad.left - pad.right;
const chartH = h - pad.top - pad.bottom;
const maxVal = Math.max(...config.data.map(d => d.value), 1);
const stepX = chartW / Math.max(config.data.length - 1, 1);
ctx.fillStyle = 'rgba(255,255,255,0.04)';
drawRoundedRect(ctx, pad.left, pad.top, chartW, chartH, 12);
ctx.fill();
if (config.showGrid) {
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
for (let i = 0; i <= 4; i++) {
const y = pad.top + (chartH / 4) * i;
ctx.beginPath();
ctx.moveTo(pad.left, y);
ctx.lineTo(pad.left + chartW, y);
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = '11px Tahoma, sans-serif';
ctx.textAlign = 'right';
ctx.fillText(String(Math.round(maxVal - (maxVal / 4) * i)), pad.left - 8, y + 4);
}
}
const points = config.data.map((point, i) => ({
x: pad.left + i * stepX,
y: pad.top + chartH - (point.value / maxVal) * chartH,
point,
i,
}));
ctx.beginPath();
ctx.strokeStyle = '#4f8cf7';
ctx.lineWidth = 3;
ctx.lineJoin = 'round';
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
const xc = (points[i].x + points[i - 1].x) / 2;
const yc = (points[i].y + points[i - 1].y) / 2;
ctx.quadraticCurveTo(points[i - 1].x, points[i - 1].y, xc, yc);
}
ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y);
ctx.stroke();
const grad = ctx.createLinearGradient(pad.left, pad.top, pad.left, pad.top + chartH);
grad.addColorStop(0, 'rgba(79,140,247,0.25)');
grad.addColorStop(1, 'rgba(79,140,247,0.01)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(points[0].x, pad.top + chartH);
for (let i = 0; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.lineTo(points[points.length - 1].x, pad.top + chartH);
ctx.closePath();
ctx.fill();
points.forEach((p) => {
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#4f8cf7';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '11px Tahoma, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(p.point.label, p.x, pad.top + chartH + 18);
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = 'bold 11px Tahoma, sans-serif';
ctx.fillText(formatNumber(p.point.value), p.x, p.y - 12);
});
}
function renderPieChart(ctx: CanvasRenderingContext2D, config: ChartConfig, w: number, h: number, isDoughnut = false) {
const cx = w / 2;
const cy = h / 2 + 10;
const radius = Math.min(w, h) / 2 - 60;
const total = config.data.reduce((s, d) => s + d.value, 0) || 1;
let startAngle = -Math.PI / 2;
config.data.forEach((point, i) => {
const sliceAngle = (point.value / total) * Math.PI * 2;
const color = point.color || getColor(i);
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
grad.addColorStop(0, color + 'cc');
grad.addColorStop(1, color);
ctx.fillStyle = grad;
ctx.fill();
if (sliceAngle > 0.3) {
const midAngle = startAngle + sliceAngle / 2;
const labelR = radius * 0.65;
const lx = cx + Math.cos(midAngle) * labelR;
const ly = cy + Math.sin(midAngle) * labelR;
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px Tahoma, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${Math.round((point.value / total) * 100)}%`, lx, ly + 4);
}
if (isDoughnut) {
ctx.beginPath();
ctx.arc(cx, cy, radius * 0.45, 0, Math.PI * 2);
ctx.fillStyle = '#0f0f14';
ctx.fill();
}
startAngle += sliceAngle;
});
if (config.showLegend) {
const legendY = h - 30;
const legendGap = 8;
let legendX = cx - ((config.data.length * 100 + (config.data.length - 1) * legendGap) / 2);
config.data.forEach((point, i) => {
const color = point.color || getColor(i);
ctx.fillStyle = color;
ctx.fillRect(legendX, legendY - 6, 10, 10);
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '11px Tahoma, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`${point.label} (${formatNumber(point.value)})`, legendX + 14, legendY + 4);
legendX += 100 + legendGap;
});
}
}
function renderAreaChart(ctx: CanvasRenderingContext2D, config: ChartConfig, w: number, h: number) {
const pad = { top: 60, right: 30, bottom: 60, left: 60 };
const chartW = w - pad.left - pad.right;
const chartH = h - pad.top - pad.bottom;
const maxVal = Math.max(...config.data.map(d => d.value), 1);
const stepX = chartW / Math.max(config.data.length - 1, 1);
const points = config.data.map((point, i) => ({
x: pad.left + i * stepX,
y: pad.top + chartH - (point.value / maxVal) * chartH,
point,
}));
const grad = ctx.createLinearGradient(pad.left, pad.top, pad.left, pad.top + chartH);
grad.addColorStop(0, 'rgba(34,197,94,0.35)');
grad.addColorStop(0.5, 'rgba(34,197,94,0.12)');
grad.addColorStop(1, 'rgba(34,197,94,0.02)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(points[0].x, pad.top + chartH);
for (let i = 0; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.lineTo(points[points.length - 1].x, pad.top + chartH);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = '#22c55e';
ctx.lineWidth = 3;
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.stroke();
points.forEach((p) => {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#22c55e';
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = 'bold 11px Tahoma, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(formatNumber(p.point.value), p.x, p.y - 10);
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '11px Tahoma, sans-serif';
ctx.fillText(p.point.label, p.x, pad.top + chartH + 18);
});
}
function formatNumber(n: number): string {
if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(1) + 'B';
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
return String(Math.round(n * 100) / 100);
}
export function renderChart(config: ChartConfig): string {
const w = config.width || 700;
const h = config.height || 400;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
if (!ctx) return '';
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = '#0f0f14';
drawRoundedRect(ctx, 0, 0, w, h, 16);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
drawRoundedRect(ctx, 0, 0, w, h, 16);
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = 'bold 16px Tahoma, sans-serif';
ctx.textAlign = 'right';
ctx.fillText(config.title, w - 20, 35);
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = '11px Tahoma, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(config.type.toUpperCase(), 20, 35);
switch (config.type) {
case 'bar':
renderBarChart(ctx, config, w, h);
break;
case 'horizontalBar':
renderHorizontalBarChart(ctx, config, w, h);
break;
case 'line':
renderLineChart(ctx, config, w, h);
break;
case 'pie':
renderPieChart(ctx, config, w, h, false);
break;
case 'doughnut':
renderPieChart(ctx, config, w, h, true);
break;
case 'area':
renderAreaChart(ctx, config, w, h);
break;
}
return canvas.toDataURL('image/png');
}
export function renderComparisonChart(datasets: { label: string; data: ChartDataPoint[]; color: string }[], title: string): string {
const w = 700;
const h = 400;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
if (!ctx) return '';
ctx.fillStyle = '#0f0f14';
drawRoundedRect(ctx, 0, 0, w, h, 16);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = 'bold 16px Tahoma, sans-serif';
ctx.textAlign = 'right';
ctx.fillText(title, w - 20, 35);
const pad = { top: 60, right: 30, bottom: 60, left: 60 };
const chartW = w - pad.left - pad.right;
const chartH = h - pad.top - pad.bottom;
const allValues = datasets.flatMap(d => d.data.map(p => p.value));
const maxVal = Math.max(...allValues, 1);
const stepX = chartW / Math.max(datasets[0]?.data.length || 1, 1);
const groupWidth = stepX * 0.6;
const barWidth = groupWidth / Math.max(datasets.length, 1) * 0.7;
datasets.forEach((ds, di) => {
ds.data.forEach((point, i) => {
const x = pad.left + i * stepX + (stepX - groupWidth) / 2 + di * (barWidth + 2);
const barH = (point.value / maxVal) * chartH;
const y = pad.top + chartH - barH;
ctx.fillStyle = ds.color;
drawRoundedRect(ctx, x, y, barWidth, barH, 3);
ctx.fill();
});
});
const legendY = h - 20;
let lx = 20;
datasets.forEach((ds) => {
ctx.fillStyle = ds.color;
ctx.fillRect(lx, legendY - 5, 10, 10);
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '11px Tahoma, sans-serif';
ctx.fillText(ds.label, lx + 14, legendY + 4);
lx += ctx.measureText(ds.label).width + 40;
});
return canvas.toDataURL('image/png');
}

Xet Storage Details

Size:
14 kB
·
Xet hash:
53bd10933e7601265c0701d156dc2dde2adfa1857d3ea10a0f2b3d88dc91036f

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.