/* global React */ /* flynas RM — formatters, hand-rolled SVG charts, primitives (global). */ const { useState, useEffect, useRef, useMemo, useLayoutEffect } = React; // ---------- Formatters (SAR) ---------- const fmtSAR = v => { if (v == null || isNaN(v)) return '—'; const a = Math.abs(v); if (a >= 1e9) return 'SAR ' + (v/1e9).toFixed(2) + 'B'; if (a >= 1e6) return 'SAR ' + (v/1e6).toFixed(1) + 'M'; if (a >= 1e3) return 'SAR ' + (v/1e3).toFixed(0) + 'K'; return 'SAR ' + Math.round(v); }; const fmtSAR0 = v => v == null ? '—' : 'SAR ' + Number(v).toLocaleString('en-US', {maximumFractionDigits: 0}); const fmtN = v => v == null ? '—' : Number(v).toLocaleString('en-US'); const fmtK = v => { if (v == null) return '—'; const a = Math.abs(v); if (a >= 1e6) return (v/1e6).toFixed(2) + 'M'; if (a >= 1e3) return (v/1e3).toFixed(1) + 'K'; return Number(v).toFixed(0); }; const fmtPct = (v, d=1) => v == null ? '—' : Number(v).toFixed(d) + '%'; const fmtSignPct = (v, d=1) => v == null ? '—' : (v > 0 ? '+' : '') + Number(v).toFixed(d) + '%'; const MONTHS = ['','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; // ---------- Delta ---------- const Delta = ({ value, suffix = '%', digits = 1, inverse = false }) => { if (value == null || isNaN(value)) return null; const pos = inverse ? value < 0 : value > 0; const neg = inverse ? value > 0 : value < 0; const cls = pos ? 'text-pos' : neg ? 'text-neg' : 'text-muted'; const arrow = value > 0 ? '▲' : value < 0 ? '▼' : '·'; return React.createElement('span', { className: `mono ${cls}`, style: { fontSize: 10.5, fontWeight: 500 } }, `${arrow} ${value > 0 ? '+' : ''}${Number(value).toFixed(digits)}${suffix}`); }; // ---------- chart sizing ---------- function useSize(ref) { const [size, setSize] = useState({ w: 0, h: 0 }); useLayoutEffect(() => { if (!ref.current) return; const ro = new ResizeObserver(es => { for (const e of es) setSize({ w: e.contentRect.width, h: e.contentRect.height }); }); ro.observe(ref.current); setSize({ w: ref.current.clientWidth, h: ref.current.clientHeight }); return () => ro.disconnect(); }, [ref]); return size; } const Sparkline = ({ data, width = 80, height = 24, color, fill = true }) => { if (!data || data.length < 2) return null; const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const dx = width / (data.length - 1); const pts = data.map((v, i) => `${i*dx},${height - ((v-min)/range)*height}`).join(' '); const c = color || 'var(--accent)'; return React.createElement('svg', { width, height, className: 'spark' }, fill && React.createElement('polygon', { points: `0,${height} ${pts} ${width},${height}`, fill: c, fillOpacity: 0.13 }), React.createElement('polyline', { points: pts, fill: 'none', stroke: c, strokeWidth: 1.4 })); }; // ---------- Bar chart (vertical / horizontal, grouped) ---------- const BarChart = ({ data, height = 240, valueKey = 'value', labelKey = 'label', format = fmtN, color, secondaryKey, secondaryColor, showLabels = true, horizontal = false, valueLabel = false, tooltipFormat, onBarClick }) => { const ref = useRef(); const { w } = useSize(ref); const padL = horizontal ? 124 : 72, padR = 16, padT = 16, padB = horizontal ? 24 : 56; const innerW = Math.max(0, w - padL - padR); const innerH = Math.max(0, height - padT - padB); const c1 = color || 'var(--ink)'; const c2 = secondaryColor || 'var(--accent)'; const grouped = !!secondaryKey; const allVals = data.flatMap(d => grouped ? [d[valueKey]||0, d[secondaryKey]||0] : [d[valueKey]||0]); const max = Math.max(...allVals, 0) * 1.12 || 1; const ticks = 4; const [hover, setHover] = useState(null); if (horizontal) { const rowH = innerH / data.length; const barH = Math.min(20, rowH * 0.66); return React.createElement('div', { ref, style: { width: '100%', height, position: 'relative' } }, React.createElement('svg', { width: w, height, style: { display: 'block' } }, Array.from({ length: ticks+1 }, (_, i) => { const x = padL + innerW*i/ticks; return React.createElement('g', { key: i }, React.createElement('line', { x1: x, x2: x, y1: padT, y2: padT+innerH, className: 'grid-line' }), React.createElement('text', { x, y: height-6, textAnchor: 'middle', className: 'axis-text' }, format(max*i/ticks))); }), data.map((d, i) => { const y = padT + i*rowH + (rowH-barH)/2; const v = d[valueKey]||0; const bw = (v/max)*innerW; return React.createElement('g', { key: i }, React.createElement('text', { x: padL-8, y: y+barH/2+3, textAnchor: 'end', className: 'axis-text' }, (d[labelKey]||'').slice(0,20)), React.createElement('rect', { x: padL, y, width: bw, height: barH, fill: c1, rx: 2, className: 'bar', onMouseEnter: e => setHover({ x: e.clientX, y: e.clientY, d, v }), onMouseLeave: () => setHover(null), style: onBarClick ? { cursor: 'pointer' } : undefined, onClick: onBarClick ? () => onBarClick(d) : undefined }), showLabels && bw > 34 && React.createElement('text', { x: padL+bw-5, y: y+barH/2+3, textAnchor: 'end', style: { fill: 'var(--paper)', fontWeight: 600, fontFamily: 'var(--mono)', fontSize: 9.5 } }, format(v))); })), hover && React.createElement('div', { className: 'tip', style: { left: hover.x - ref.current.getBoundingClientRect().left + 10, top: hover.y - ref.current.getBoundingClientRect().top - 30 } }, `${hover.d[labelKey]}: ${format(hover.v)}`)); } const groupW = innerW / data.length; const barW = grouped ? Math.min(17, groupW*0.36) : Math.min(30, groupW*0.62); const gap = grouped ? 2 : 0; // signed scale with a zero baseline so negative bars (e.g. Δ revenue) render downward const topV = (Math.max(...allVals, 0) || 1) * 1.08; const botV = Math.min(...allVals, 0) * 1.08; const span = (topV - botV) || 1; const yOf = v => padT + innerH - ((v - botV) / span) * innerH; const zeroY = yOf(0); return React.createElement('div', { ref, style: { width: '100%', height, position: 'relative' } }, React.createElement('svg', { width: w, height, style: { display: 'block' } }, Array.from({ length: ticks+1 }, (_, i) => { const tv = botV + span*i/ticks; const y = yOf(tv); return React.createElement('g', { key: i }, React.createElement('line', { x1: padL, x2: padL+innerW, y1: y, y2: y, className: 'grid-line' }), React.createElement('text', { x: padL-6, y: y+3, textAnchor: 'end', className: 'axis-text' }, format(tv))); }), data.map((d, i) => { const cx = padL + i*groupW + groupW/2; const v1 = d[valueKey]||0; const v2 = grouped ? (d[secondaryKey]||0) : 0; const y1 = Math.min(yOf(v1), zeroY), h1 = Math.abs(yOf(v1) - zeroY); const y2 = Math.min(yOf(v2), zeroY), h2 = Math.abs(yOf(v2) - zeroY); const x1 = grouped ? cx-barW-gap/2 : cx-barW/2; const x2 = grouped ? cx+gap/2 : cx; const label = (d[labelKey]||'').toString(); const fill1 = v1 < 0 ? 'var(--neg)' : c1; return React.createElement('g', { key: i }, React.createElement('rect', { x: x1, y: y1, width: barW, height: Math.max(0.5, h1), fill: fill1, rx: 2, className: 'bar', style: onBarClick ? { cursor: 'pointer' } : undefined, onMouseEnter: e => setHover({ x: e.clientX, y: e.clientY, d, label, v: v1, k: valueKey }), onMouseLeave: () => setHover(null), onClick: onBarClick ? () => onBarClick(d) : undefined }), valueLabel && React.createElement('text', { x: x1+barW/2, y: v1 >= 0 ? y1-4 : y1+h1+11, textAnchor: 'middle', style: { fontFamily: 'var(--mono)', fontSize: 10, fontWeight: 600, fill: 'var(--text-dim)' } }, format(v1)), grouped && React.createElement('rect', { x: x2, y: y2, width: barW, height: Math.max(0.5, h2), fill: c2, rx: 2, className: 'bar', onMouseEnter: e => setHover({ x: e.clientX, y: e.clientY, d, label, v: v2, k: secondaryKey }), onMouseLeave: () => setHover(null) }), React.createElement('text', { x: cx, y: padT+innerH+14, textAnchor: label.length > 4 ? 'end' : 'middle', className: 'axis-text', transform: label.length > 4 ? `rotate(-32 ${cx} ${padT+innerH+14})` : '' }, label.length > 14 ? label.slice(0,13)+'…' : label)); })), hover && (() => { const body = tooltipFormat ? tooltipFormat(hover.d, hover.k) : `${hover.label}: ${format(hover.v)}`; return React.createElement('div', { className: 'tip', style: { left: hover.x - ref.current.getBoundingClientRect().left + 10, top: hover.y - ref.current.getBoundingClientRect().top - 30 } }, body); })()); }; // ---------- Composed: bars + line ---------- const ComposedChart = ({ data, height = 280, barKey, lineKey, labelKey = 'label', barFormat = fmtSAR, lineFormat = v => Number(v).toFixed(0)+'%', barColor = 'var(--ink)', lineColor = 'var(--accent)', barLabel = 'Bar', lineLabel = 'Line' }) => { const ref = useRef(); const { w } = useSize(ref); const padL = 78, padR = 54, padT = 18, padB = 34; const innerW = Math.max(0, w-padL-padR); const innerH = Math.max(0, height-padT-padB); const barMax = Math.max(...data.map(d => d[barKey]||0))*1.15 || 1; const lineVals = data.map(d => d[lineKey]||0); const lineMin = Math.min(...lineVals)*0.92; const lineMax = Math.max(...lineVals)*1.04 || 1; const lineRange = lineMax-lineMin || 1; const groupW = innerW/data.length; const barW = Math.min(26, groupW*0.55); const ticks = 4; const [hover, setHover] = useState(null); const linePts = data.map((d, i) => { const x = padL + i*groupW + groupW/2; const v = d[lineKey]||0; return { x, y: padT+innerH-((v-lineMin)/lineRange)*innerH, v, d }; }); return React.createElement('div', { ref, style: { width: '100%', height, position: 'relative' } }, React.createElement('svg', { width: w, height, style: { display: 'block' } }, Array.from({ length: ticks+1 }, (_, i) => { const y = padT+innerH-innerH*i/ticks; return React.createElement('g', { key: i }, React.createElement('line', { x1: padL, x2: padL+innerW, y1: y, y2: y, className: 'grid-line' }), React.createElement('text', { x: padL-6, y: y+3, textAnchor: 'end', className: 'axis-text' }, barFormat(barMax*i/ticks)), React.createElement('text', { x: padL+innerW+6, y: y+3, textAnchor: 'start', className: 'axis-text' }, lineFormat(lineMin+lineRange*i/ticks))); }), data.map((d, i) => { const cx = padL+i*groupW+groupW/2; const v = d[barKey]||0; const h = (v/barMax)*innerH; return React.createElement('g', { key: 'b'+i }, React.createElement('rect', { x: cx-barW/2, y: padT+innerH-h, width: barW, height: h, fill: barColor, rx: 2, className: 'bar', opacity: 0.92, onMouseEnter: e => setHover({ x: e.clientX, y: e.clientY, d }), onMouseLeave: () => setHover(null) }), (i % Math.ceil(data.length / 13) === 0) && React.createElement('text', { x: cx, y: padT+innerH+(data.length > 10 ? 12 : 14), textAnchor: data.length > 10 ? 'end' : 'middle', className: 'axis-text', transform: data.length > 10 ? `rotate(-32 ${cx} ${padT+innerH+12})` : '' }, d[labelKey])); }), React.createElement('polyline', { points: linePts.map(p => `${p.x},${p.y}`).join(' '), fill: 'none', stroke: lineColor, strokeWidth: 1.9 }), linePts.map((p, i) => React.createElement('circle', { key: 'p'+i, cx: p.x, cy: p.y, r: 3, fill: lineColor }))), hover && React.createElement('div', { className: 'tip', style: { left: Math.max(0, hover.x - ref.current.getBoundingClientRect().left - 60), top: hover.y - ref.current.getBoundingClientRect().top - 50 } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 4, color: 'var(--paper)' } }, hover.d[labelKey]), React.createElement('div', { style: { color: 'var(--paper)' } }, `${barLabel}: ${barFormat(hover.d[barKey])}`), React.createElement('div', { style: { color: 'var(--paper)', marginTop: 2 } }, `${lineLabel}: ${lineFormat(hover.d[lineKey])}`))); }; function _smooth(points, tension = 0.2) { if (!points || points.length < 2) return points && points.length ? `M${points[0][0]},${points[0][1]}` : ''; let d = `M${points[0][0]},${points[0][1]}`; for (let i = 0; i < points.length-1; i++) { const p0 = points[i-1]||points[i], p1 = points[i], p2 = points[i+1], p3 = points[i+2]||p2; d += ` C${p1[0]+(p2[0]-p0[0])*tension},${p1[1]+(p2[1]-p0[1])*tension} ${p2[0]-(p3[0]-p1[0])*tension},${p2[1]-(p3[1]-p1[1])*tension} ${p2[0]},${p2[1]}`; } return d; } // ---------- Line chart (multi-series, optional smooth) ---------- const LineChart = ({ data, height = 240, series = [], xKey = 'label', format = fmtN, xFormat = v => v, smooth = false }) => { const ref = useRef(); const { w } = useSize(ref); const padL = 60, padR = 18, padT = 16, padB = 28; const innerW = Math.max(0, w-padL-padR); const innerH = Math.max(0, height-padT-padB); const valid = v => v != null && !isNaN(v); const allVals = data.flatMap(d => series.map(s => d[s.key])).filter(valid); const min = Math.min(...allVals, 0); const max = (allVals.length ? Math.max(...allVals) : 1)*1.05 || 1; const range = max-min || 1; const dx = data.length > 1 ? innerW/(data.length-1) : innerW; const ticks = 4; const [hover, setHover] = useState(null); const yFor = v => padT+innerH-((v-min)/range)*innerH; const xFor = i => padL+i*dx; return React.createElement('div', { ref, style: { width: '100%', height, position: 'relative' } }, React.createElement('svg', { width: w, height, style: { display: 'block' } }, Array.from({ length: ticks+1 }, (_, i) => { const y = padT+innerH-innerH*i/ticks; return React.createElement('g', { key: i }, React.createElement('line', { x1: padL, x2: padL+innerW, y1: y, y2: y, className: 'grid-line' }), React.createElement('text', { x: padL-6, y: y+3, textAnchor: 'end', className: 'axis-text' }, format(min+range*i/ticks))); }), data.map((d, i) => i % Math.ceil(data.length/8) === 0 && React.createElement('text', { key: 'x'+i, x: xFor(i), y: padT+innerH+14, textAnchor: 'middle', className: 'axis-text' }, xFormat(d[xKey]))), series.map((s, si) => { const pairs = data.map((d, i) => valid(d[s.key]) ? [xFor(i), yFor(d[s.key])] : null).filter(Boolean); const ptsStr = pairs.map(p => p.join(',')).join(' '); const lastIdx = data.reduce((a, d, i) => valid(d[s.key]) ? i : a, -1); const firstIdx = data.findIndex(d => valid(d[s.key])); const useSm = smooth && pairs.length > 1; const path = useSm ? _smooth(pairs) : null; return React.createElement('g', { key: si }, s.fill && pairs.length > 0 && (useSm ? React.createElement('path', { d: `${path} L${xFor(lastIdx)},${padT+innerH} L${xFor(firstIdx)},${padT+innerH} Z`, fill: s.color, fillOpacity: 0.12, stroke: 'none' }) : React.createElement('polygon', { points: `${xFor(firstIdx)},${padT+innerH} ${ptsStr} ${xFor(lastIdx)},${padT+innerH}`, fill: s.color, fillOpacity: 0.12 })), pairs.length > 1 && (useSm ? React.createElement('path', { d: path, fill: 'none', stroke: s.color, strokeWidth: s.width||1.7, strokeDasharray: s.dashed ? '7 4' : '', strokeLinecap: 'round' }) : React.createElement('polyline', { points: ptsStr, fill: 'none', stroke: s.color, strokeWidth: s.width||1.7, strokeDasharray: s.dashed ? '7 4' : '' }))); }), data.map((d, i) => React.createElement('rect', { key: 'h'+i, x: xFor(i)-dx/2, y: padT, width: dx, height: innerH, fill: 'transparent', onMouseEnter: e => setHover({ x: e.clientX, y: e.clientY, d, i }), onMouseLeave: () => setHover(null) }))), hover && React.createElement('div', { className: 'tip', style: { left: Math.min(innerW-40, Math.max(0, xFor(hover.i)-50)), top: padT } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 4, color: 'var(--paper)' } }, xFormat(hover.d[xKey])), series.map((s, si) => React.createElement('div', { key: si, style: { display: 'flex', alignItems: 'center', gap: 6, color: 'var(--paper)', marginTop: si ? 2 : 0 } }, React.createElement('span', { style: { width: 10, height: 3, background: s.color, flexShrink: 0 } }), valid(hover.d[s.key]) ? `${s.label}: ${format(hover.d[s.key])}` : `${s.label}: —`))), React.createElement('div', { style: { position: 'absolute', top: 2, right: 12, display: 'flex', gap: 12, fontSize: 10 } }, series.map((s, si) => React.createElement('div', { key: si, style: { display: 'flex', alignItems: 'center', gap: 5, color: 'var(--text-dim)' } }, React.createElement('svg', { width: 18, height: 4 }, React.createElement('line', { x1: 0, y1: 2, x2: 18, y2: 2, stroke: s.color, strokeWidth: s.width||1.7, strokeDasharray: s.dashed ? '4 2' : 'none' })), s.label)))); }; // ---------- Chart from a copilot spec ({type,title,x,series,data}) ---------- const ChartFromSpec = ({ spec }) => { if (!spec || !spec.data || !spec.data.length) return null; const data = spec.data; const x = spec.x || 'label'; const series = (spec.series || []).map((s, i) => ({ key: s.key, label: s.label || s.key, color: i === 0 ? 'var(--accent)' : i === 1 ? 'var(--ink)' : 'var(--amber)', fill: spec.type === 'line', width: 1.8 })); const moneyish = JSON.stringify(spec).toLowerCase().match(/revenue|sar|fare|yield/) ? true : false; const fmt = moneyish ? fmtSAR : fmtN; const norm = data.map(d => ({ ...d, label: d[x] })); const body = spec.type === 'line' ? React.createElement(LineChart, { data: norm, series, xKey: 'label', format: fmt, height: 220, smooth: true }) : spec.type === 'grouped-bar' && series.length > 1 ? React.createElement(BarChart, { data: norm, valueKey: series[0].key, secondaryKey: series[1].key, secondaryColor: 'var(--ink)', color: 'var(--accent)', format: fmt, height: 220 }) : React.createElement(BarChart, { data: norm, valueKey: series[0] ? series[0].key : 'value', color: 'var(--accent)', format: fmt, height: 220 }); return React.createElement('div', { style: { marginTop: 8, border: '1px solid var(--rule-soft)', borderRadius: 4, padding: '8px 8px 4px', background: 'var(--paper)' } }, spec.title && React.createElement('div', { className: 'label', style: { padding: '2px 4px 6px' } }, spec.title), body); }; // ---------- heat helpers ---------- const heatColor = (v, opts = {}) => { const { mid = 0, max = 10 } = opts; const t = Math.max(-1, Math.min(1, (v-mid)/max)); if (Math.abs(t) < 0.08) return { bg: 'var(--paper-edge)', fg: 'var(--text-dim)' }; const a = 0.15 + Math.abs(t)*0.55; if (t > 0) return { bg: `color-mix(in srgb, var(--green) ${(a*100).toFixed(0)}%, var(--paper-soft))`, fg: a > 0.45 ? '#fff' : 'var(--green)' }; return { bg: `color-mix(in srgb, var(--neg) ${(a*100).toFixed(0)}%, var(--paper-soft))`, fg: a > 0.45 ? '#fff' : 'var(--neg)' }; }; const occColor = v => { if (v == null) return { bg: 'var(--paper-edge)', fg: 'var(--text-dim)' }; if (v >= 92) return { bg: 'var(--accent)', fg: '#fff' }; if (v >= 82) return { bg: 'color-mix(in srgb, var(--accent) 68%, var(--paper))', fg: '#fff' }; if (v >= 70) return { bg: 'color-mix(in srgb, var(--accent) 38%, var(--paper))', fg: 'var(--ink)' }; if (v >= 55) return { bg: 'color-mix(in srgb, var(--accent) 16%, var(--paper))', fg: 'var(--ink)' }; return { bg: 'var(--paper-edge)', fg: 'var(--text-dim)' }; }; // ---------- primitives ---------- const Panel = ({ title, sub, actions, children, flush = false, className = '' }) => React.createElement('div', { className: `panel ${className}` }, (title || actions) && React.createElement('div', { className: 'panel-head' }, React.createElement('div', null, title && React.createElement('div', { className: 'panel-head-title' }, title), sub && React.createElement('div', { className: 'panel-head-sub' }, sub)), actions && React.createElement('div', { className: 'row gap-2' }, actions)), React.createElement('div', { className: flush ? 'panel-body-flush' : 'panel-body' }, children)); const KPI = ({ label, value, sub, delta, deltaInverse, sparkline, sparklineColor }) => React.createElement('div', { className: 'kpi' }, React.createElement('div', { className: 'kpi-label' }, label), React.createElement('div', { className: 'kpi-value' }, value), React.createElement('div', { className: 'kpi-sub' }, sub && React.createElement('span', null, sub), delta != null && React.createElement(Delta, { value: delta, inverse: deltaInverse })), sparkline && React.createElement('div', { style: { marginTop: 4 } }, React.createElement(Sparkline, { data: sparkline, width: 84, height: 26, color: sparklineColor || 'var(--accent)' }))); const Pill = ({ kind = 'info', children }) => React.createElement('span', { className: `pill pill-${kind}` }, children); Object.assign(window, { fmtSAR, fmtSAR0, fmtN, fmtK, fmtPct, fmtSignPct, MONTHS, Delta, Sparkline, BarChart, ComposedChart, LineChart, ChartFromSpec, heatColor, occColor, Panel, KPI, Pill, useSize, useState, useEffect, useRef, useMemo, useLayoutEffect, });