Buckets:
| 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.