// Dashboard - 数据看板 const { useState, useMemo } = React; const D_ = window.__CARBON_DATA; // SVG doughnut chart function Doughnut({ data, total }) { const r = 65, c = 80; const circumference = 2 * Math.PI * r; let offset = 0; return (
{data.map((d, i) => { const len = (d.value / total) * circumference; const dash = `${len} ${circumference - len}`; const el = ; offset += len; return el; })}
{fmt(total / 10000, 2)}
万 tCO₂e
); } // SVG bar chart function BarChart({ data, height = 220, color = 'var(--c-primary)' }) { const max = Math.max(...data.map(d => d.value), 1); const w = 720, padL = 40, padR = 16, padB = 32, padT = 16; const chartW = w - padL - padR, chartH = height - padT - padB; const barW = chartW / data.length * 0.6; const gap = chartW / data.length; return ( {/* y-axis grid */} {[0, 0.25, 0.5, 0.75, 1].map((t, i) => { const y = padT + chartH * (1 - t); return ( {fmt(max * t / 1000, 1)}k ); })} {data.map((d, i) => { const h = chartH * (d.value / max); const x = padL + i * gap + (gap - barW) / 2; const y = padT + chartH - h; return ( {d.label}: {fmt(d.value, 2)} tCO₂e {d.label.slice(-5)} ); })} ); } // Stacked bar (scope1/2/3 per month) function StackedBar({ data, height = 280 }) { const max = Math.max(...data.map(d => d.s1 + d.s2 + d.s3), 1); const w = 720, padL = 50, padR = 16, padB = 36, padT = 16; const chartW = w - padL - padR, chartH = height - padT - padB; const barW = chartW / data.length * 0.65; const gap = chartW / data.length; const colors = ['var(--c-scope1)', 'var(--c-scope2)', 'var(--c-scope3)']; return ( {[0, 0.25, 0.5, 0.75, 1].map((t, i) => { const y = padT + chartH * (1 - t); return ( {fmt(max * t / 1000, 0)}k ); })} {data.map((d, i) => { const x = padL + i * gap + (gap - barW) / 2; const segs = [d.s1, d.s2, d.s3]; let yCursor = padT + chartH; return ( {segs.map((v, j) => { const h = chartH * (v / max); yCursor -= h; return ( 范围{j+1}: {fmt(v, 2)} tCO₂e ); })} {d.label.slice(-5)} ); })} ); } // Horizontal bar (orgs) function HBar({ data, max }) { const m = max || Math.max(...data.map(d => d.value), 1); return (
{data.map((d, i) => (
{d.label} {fmt(d.value, 1)} tCO₂e
))}
); } function Dashboard() { const { orgId, org, path } = useOrg(); const [yearRange, setYearRange] = useState('12m'); const periods = D_.PERIODS; const periodFilter = { start: periods[0], end: periods[periods.length - 1] }; const cur = useMemo(() => D_.aggregate(orgId, periodFilter), [orgId]); // Compare with prior period (first half vs second half of 12 months) const half = Math.floor(periods.length / 2); const prior = D_.aggregate(orgId, { start: periods[0], end: periods[half - 1] }); const latest = D_.aggregate(orgId, { start: periods[half], end: periods[periods.length - 1] }); const delta = prior.total > 0 ? (latest.total - prior.total) / prior.total * 100 : 0; // monthly trend (stacked) const monthly = useMemo(() => { return periods.map(p => { const entries = cur.entries.filter(e => e.period === p); const s = { label: p, s1: 0, s2: 0, s3: 0 }; entries.forEach(e => { s['s' + e.scope] += e.emission; }); return s; }); }, [cur.entries, periods]); // by category const byCategory = D_.EMISSION_CATEGORIES.map(c => ({ label: c.name, value: cur.byCategory[c.code] || 0, color: c.color, scope: c.scope })).filter(c => c.value > 0).sort((a, b) => b.value - a.value); // by sub-org (children of current) const childOrgs = D_.ORG_FLAT.filter(o => o.parentId === orgId); const subOrgData = childOrgs.map(c => { const agg = D_.aggregate(c.id, periodFilter); return { label: c.name, value: agg.total, color: 'var(--c-primary)' }; }).filter(d => d.value > 0).sort((a, b) => b.value - a.value); const scopeData = [ { label: '范围一', value: cur.byScope[1] || 0, color: 'var(--c-scope1)' }, { label: '范围二', value: cur.byScope[2] || 0, color: 'var(--c-scope2)' }, { label: '范围三', value: cur.byScope[3] || 0, color: 'var(--c-scope3)' } ]; // 排放强度 (per employee) const intensity = (org?.employees || 1) > 0 ? cur.total / (org.employees || 1) : 0; return ( <>