148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
/**
|
|
* 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,
|
|
}
|
|
}
|