WIP jordan

This commit is contained in:
Jordan Weingarten
2026-04-16 11:11:44 -05:00
parent ff2d7b18b5
commit 67482f6629
163 changed files with 50627 additions and 728 deletions

View File

@@ -0,0 +1,147 @@
/**
* 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<AnalyticsState>('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,
}
}