/** * Business Analytics — composable for chart state, SVG rendering, and domain filtering. * SVG chart math extracted from /app/pages/index.vue dashboard charts. */ import { useLocalStorageRef } from '~/utils/useLocalStorageRef' import { ANALYTICS_METRICS, ANALYTICS_KPI_SUMMARIES, type AnalyticsDomainId, type AnalyticsChartType, type AnalyticsTimePoint, } from '~/data/mock-analytics' export interface ChartSvgModel { lineD: string areaD: string points: { x: number; y: number; v: number }[] bars: { x: number; y: number; w: number; h: number }[] gridYs: number[] viewW: number viewH: number padX: number innerW: number bottomY: number } interface AnalyticsState { activeDomain: AnalyticsDomainId chartBuilderMetric: string chartBuilderType: AnalyticsChartType chartBuilderRange: '3m' | '6m' | '12m' } function buildDefaults(): AnalyticsState { return { activeDomain: 'production', chartBuilderMetric: 'gwp', chartBuilderType: 'area', chartBuilderRange: '6m', } } export function useAnalytics() { const state = useLocalStorageRef('policy-ui-analytics-v1', buildDefaults) const allMetrics = ANALYTICS_METRICS const kpiSummaries = ANALYTICS_KPI_SUMMARIES const domainMetrics = computed(() => allMetrics.filter(m => m.domain === state.value.activeDomain) ) const chartBuilderMetricObj = computed(() => allMetrics.find(m => m.id === state.value.chartBuilderMetric) ?? allMetrics[0]! ) const chartBuilderData = computed(() => { const data = chartBuilderMetricObj.value.data12m.filter(d => d.m) const range = state.value.chartBuilderRange if (range === '3m') return data.slice(-3) if (range === '6m') return data.slice(-6) return data }) // ── SVG chart model builder (extracted from dashboard index.vue) ── function buildSvgModel(data: AnalyticsTimePoint[], viewW = 400, viewH = 120): ChartSvgModel { const pts = data.map(d => d.v) const padX = 8; const padY = 14 const innerW = viewW - padX * 2; const innerH = viewH - padY * 2 const maxV = Math.max(...pts, 1); const minV = Math.min(...pts, 0) const span = maxV - minV || 1 const points = pts.map((p, i) => ({ x: padX + (i / Math.max(1, pts.length - 1)) * innerW, y: padY + (1 - (p - minV) / span) * innerH, v: p, })) const bottomY = padY + innerH const first = points[0]!; const last = points[points.length - 1]! // Line + area paths (smooth Bézier curves) let lineD = `M ${first.x},${first.y}` let areaD = `M ${first.x},${bottomY} L ${first.x},${first.y}` for (let i = 1; i < points.length; i++) { const p0 = points[i - 1]!; const p1 = points[i]! const cx = (p0.x + p1.x) / 2 const seg = ` C ${cx},${p0.y} ${cx},${p1.y} ${p1.x},${p1.y}` lineD += seg; areaD += seg } areaD += ` L ${last.x},${bottomY} Z` // Bar geometry const barW = Math.min(innerW / pts.length * 0.6, 40) const bars = pts.map((p, i) => ({ x: padX + (i / Math.max(1, pts.length - 1)) * innerW - barW / 2, y: padY + (1 - (p - minV) / span) * innerH, w: barW, h: ((p - minV) / span) * innerH, })) const gridYs = [0, 0.5, 1].map(t => padY + t * innerH) return { lineD, areaD, points, bars, gridYs, viewW, viewH, padX, innerW, bottomY } } // ── Sparkline helpers ── function sparklinePath(pts: number[], w = 112, h = 32, pad = 2): string { const max = Math.max(...pts); const min = Math.min(...pts); const r = max - min || 1 const mapped = pts.map((p, i, arr) => ({ x: pad + (i / Math.max(1, arr.length - 1)) * (w - pad * 2), y: pad + (1 - (p - min) / r) * (h - pad * 2), })) if (mapped.length < 2) return '' let d = `M ${mapped[0]!.x},${mapped[0]!.y}` for (let i = 0; i < mapped.length - 1; i++) { const p0 = mapped[i]!; const p1 = mapped[i + 1]!; const cx = (p0.x + p1.x) / 2 d += ` C ${cx},${p0.y} ${cx},${p1.y} ${p1.x},${p1.y}` } return d } function sparklineArea(pts: number[], w = 112, h = 32, pad = 2): string { const path = sparklinePath(pts, w, h, pad) if (!path) return '' const max = Math.max(...pts); const min = Math.min(...pts); const r = max - min || 1 const mapped = pts.map((p, i, arr) => ({ x: pad + (i / Math.max(1, arr.length - 1)) * (w - pad * 2), y: pad + (1 - (p - min) / r) * (h - pad * 2), })) return `${path} L ${mapped[mapped.length - 1]!.x},${h} L ${mapped[0]!.x},${h} Z` } const chartBuilderSvgModel = computed(() => buildSvgModel(chartBuilderData.value, 400, 180) ) return { state, allMetrics, kpiSummaries, domainMetrics, chartBuilderMetricObj, chartBuilderData, chartBuilderSvgModel, buildSvgModel, sparklinePath, sparklineArea, } }