WIP jordan
This commit is contained in:
147
app/composables/useAnalytics.ts
Normal file
147
app/composables/useAnalytics.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user