{"spec_id":"chernoff-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// chernoff-basic: Chernoff Faces for Multivariate Data\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\nconst SPEC_TITLE = \"Company Financial Health\";\nconst TITLE = `${SPEC_TITLE} · chernoff-basic · javascript · muix · anyplot.ai`;\n// Title fontsize scales linearly off the 67-char baseline (default 22px, floor 15px).\nconst TITLE_FONT_SIZE = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));\nconst SUBTITLE = \"Twelve companies across three sectors — each face is one company, each feature is one metric\";\n\n// --- Data: 12 companies across 3 sectors, 7 metrics normalized to [0, 1] ----\n// Small fixed-seed LCG — the browser has no seeded RNG.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\nconst clamp01 = (v) => Math.min(1, Math.max(0, v));\nconst jitter = () => (rand() - 0.5) * 0.44;\n\nconst SECTORS = [\"Technology\", \"Retail\", \"Manufacturing\"];\n\n// Sector-level baselines that make the three rows read as distinct visual\n// families before any per-company jitter is applied.\nconst SECTOR_PROFILE = {\n  Technology: { revenueGrowth: 0.85, profitMargin: 0.65, liquidityRatio: 0.55, marketShare: 0.4, debtRatio: 0.15, innovationIndex: 0.9, employeeRetention: 0.85 },\n  Retail: { revenueGrowth: 0.4, profitMargin: 0.28, liquidityRatio: 0.72, marketShare: 0.68, debtRatio: 0.45, innovationIndex: 0.28, employeeRetention: 0.48 },\n  Manufacturing: { revenueGrowth: 0.22, profitMargin: 0.38, liquidityRatio: 0.32, marketShare: 0.55, debtRatio: 0.85, innovationIndex: 0.5, employeeRetention: 0.25 },\n};\n\nconst COMPANIES = [\n  { name: \"Nova Systems\", sector: \"Technology\" },\n  { name: \"Quantum Byte\", sector: \"Technology\" },\n  { name: \"CloudPeak\", sector: \"Technology\" },\n  { name: \"Vertex Labs\", sector: \"Technology\" },\n  { name: \"Harborline Retail\", sector: \"Retail\" },\n  { name: \"Meadow Mart\", sector: \"Retail\" },\n  { name: \"Urban Goods\", sector: \"Retail\" },\n  { name: \"Riverside Shops\", sector: \"Retail\" },\n  { name: \"IronWorks Mfg\", sector: \"Manufacturing\" },\n  { name: \"Steelframe Co\", sector: \"Manufacturing\" },\n  { name: \"Forge Dynamics\", sector: \"Manufacturing\" },\n  { name: \"Anvil Industries\", sector: \"Manufacturing\" },\n];\n\nconst METRIC_KEYS = [\"revenueGrowth\", \"profitMargin\", \"liquidityRatio\", \"marketShare\", \"debtRatio\", \"innovationIndex\", \"employeeRetention\"];\n\nconst faces = COMPANIES.map((c, i) => {\n  const base = SECTOR_PROFILE[c.sector];\n  const metrics = {};\n  METRIC_KEYS.forEach((k) => {\n    metrics[k] = clamp01(base[k] + jitter());\n  });\n  return {\n    ...c,\n    row: SECTORS.indexOf(c.sector),\n    col: i % 4,\n    metrics,\n  };\n});\n\n// --- Facial feature mapping (documented, not arbitrary) ----------------------\n// face width      <- revenueGrowth      face height   <- profitMargin\n// eye size        <- liquidityRatio     eye spacing   <- marketShare\n// eyebrow angle   <- debtRatio (higher debt -> more furrowed / worried brow)\n// nose length     <- innovationIndex\n// mouth curvature <- employeeRetention (higher retention -> bigger smile)\nconst FACE_RX = 62;\nconst FACE_RY = 78;\nconst EYE_R = 9;\nconst EYE_DX = 22;\nconst BROW_LEN = 26;\nconst NOSE_LEN = 22;\nconst MOUTH_HALF_W = 26;\nconst MOUTH_CURVE_MAX = 26;\n\nfunction FaceGlyph({ face, cx, cy, color }) {\n  const m = face.metrics;\n  const rx = FACE_RX * (0.72 + 0.56 * m.revenueGrowth);\n  const ry = FACE_RY * (0.75 + 0.5 * m.profitMargin);\n  const eyeR = EYE_R * (0.6 + 0.8 * m.liquidityRatio);\n  const eyeDx = EYE_DX * (0.7 + 0.6 * m.marketShare);\n  const browAngle = -8 + 36 * m.debtRatio; // degrees; negative = relaxed/raised, positive = furrowed as debt rises\n  const noseLen = NOSE_LEN * (0.6 + 0.8 * m.innovationIndex);\n  const mouthCurve = (m.employeeRetention - 0.5) * 2 * MOUTH_CURVE_MAX; // + = smile, - = frown\n\n  const eyeY = cy - ry * 0.12;\n  const browY = eyeY - eyeR - 8;\n  const noseY0 = cy + ry * 0.05;\n  const noseY1 = noseY0 + noseLen;\n  const mouthY = cy + ry * 0.48;\n\n  return (\n    <g>\n      <ellipse cx={cx} cy={cy} rx={rx} ry={ry} fill={t.elevatedBg} stroke={color} strokeWidth={3.5} />\n      <circle cx={cx - eyeDx} cy={eyeY} r={eyeR} fill={t.ink} />\n      <circle cx={cx + eyeDx} cy={eyeY} r={eyeR} fill={t.ink} />\n      <line x1={cx - eyeDx - BROW_LEN / 2} y1={browY} x2={cx - eyeDx + BROW_LEN / 2} y2={browY} stroke={t.ink} strokeWidth={3} strokeLinecap=\"round\" transform={`rotate(${browAngle} ${cx - eyeDx} ${browY})`} />\n      <line x1={cx + eyeDx - BROW_LEN / 2} y1={browY} x2={cx + eyeDx + BROW_LEN / 2} y2={browY} stroke={t.ink} strokeWidth={3} strokeLinecap=\"round\" transform={`rotate(${-browAngle} ${cx + eyeDx} ${browY})`} />\n      <path d={`M ${cx} ${noseY0} L ${cx} ${noseY1} L ${cx + 4} ${noseY1 + 3}`} fill=\"none\" stroke={t.inkSoft} strokeWidth={2.5} strokeLinecap=\"round\" />\n      <path\n        d={`M ${cx - MOUTH_HALF_W} ${mouthY} Q ${cx} ${mouthY + mouthCurve} ${cx + MOUTH_HALF_W} ${mouthY}`}\n        fill=\"none\"\n        stroke={t.ink}\n        strokeWidth={3}\n        strokeLinecap=\"round\"\n      />\n      <text x={cx} y={cy + ry + 26} textAnchor=\"middle\" fontSize={13} fill={t.inkSoft}>\n        {face.name}\n      </text>\n    </g>\n  );\n}\n\nfunction FaceGrid() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g>\n      {faces.map((face) => (\n        <FaceGlyph key={face.name} face={face} cx={xs(face.col + 0.5)} cy={ys(face.row + 0.5)} color={t.palette[face.row]} />\n      ))}\n    </g>\n  );\n}\n\nfunction SectorLegend() {\n  const { width } = window.ANYPLOT_SIZE;\n  const swatch = 14;\n  const gap = 10;\n  const groupGap = 32;\n  const fontSize = 14;\n  const charW = fontSize * 0.58;\n  const widths = SECTORS.map((s) => swatch + gap + s.length * charW);\n  const totalWidth = widths.reduce((a, b) => a + b, 0) + groupGap * (SECTORS.length - 1);\n  let x = width / 2 - totalWidth / 2;\n  const y = 108;\n  return (\n    <g fontSize={fontSize} fill={t.inkSoft}>\n      {SECTORS.map((s, i) => {\n        const rectX = x;\n        const labelX = rectX + swatch + gap;\n        x += widths[i] + groupGap;\n        return (\n          <g key={s}>\n            <rect x={rectX} y={y - swatch + 3} width={swatch} height={swatch} rx={3} fill={t.palette[i]} />\n            <text x={labelX} y={y}>\n              {s}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction FeatureKey() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const lines = [\"Face width → revenue growth · Face height → profit margin · Eye size → liquidity ratio · Eye spacing → market share\", \"Eyebrow angle → debt ratio · Nose length → innovation index · Smile → employee retention\"];\n  return (\n    <g fontSize={12} fill={t.inkSoft} textAnchor=\"middle\">\n      {lines.map((line, i) => (\n        <text key={line} x={width / 2} y={height - 34 + i * 18}>\n          {line}\n        </text>\n      ))}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const MARGIN = { top: 190, right: 60, bottom: 80, left: 60 };\n  const cols = 4;\n  const rows = SECTORS.length;\n\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      series={[]}\n      margin={MARGIN}\n      xAxis={[{ scaleType: \"linear\", min: 0, max: cols, disableLine: true, disableTicks: true, valueFormatter: () => \"\" }]}\n      yAxis={[{ scaleType: \"linear\", min: 0, max: rows, reverse: true, disableLine: true, disableTicks: true, valueFormatter: () => \"\" }]}\n      skipAnimation\n    >\n      <FaceGrid />\n      <text x={width / 2} y={44} textAnchor=\"middle\" fontSize={TITLE_FONT_SIZE} fontWeight={600} fill={t.ink}>\n        {TITLE}\n      </text>\n      <text x={width / 2} y={72} textAnchor=\"middle\" fontSize={15} fill={t.inkSoft}>\n        {SUBTITLE}\n      </text>\n      <SectorLegend />\n      <FeatureKey />\n    </ChartContainer>\n  );\n}\n"}