{"spec_id":"chernoff-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// chernoff-basic: Chernoff Faces for Multivariate Data\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Eight financial-health metrics per company, each mapped to a distinct\n// facial feature. A tiny LCG stands in for a seeded RNG (the browser has\n// none).\nlet lcgState = 42;\nfunction lcg() {\n  lcgState = (lcgState * 1103515245 + 12345) % 2147483648;\n  return lcgState / 2147483648;\n}\n// Stretches a 0-1 LCG draw into a realistic domain range for metrics whose\n// natural units aren't a 0-100% figure (e.g. a liquidity ratio or a\n// debt-to-equity multiple).\nfunction scaleRange(v, min, max) {\n  return min + v * (max - min);\n}\n\nconst sectors = [\n  { name: \"Technology\", color: t.palette[0] },\n  { name: \"Retail\", color: t.palette[1] },\n  { name: \"Energy\", color: t.palette[2] },\n];\n\nconst companyNames = [\n  \"Cedar Systems\",\n  \"Harbor Robotics\",\n  \"Nimbus Cloudworks\",\n  \"Bluepeak Retail\",\n  \"Marlowe & Finch\",\n  \"Driftwood Goods\",\n  \"Solara Power\",\n  \"Ferro Energy\",\n  \"Tidewater Fuels\",\n  \"Vantage Analytics\",\n  \"Coral Mercantile\",\n  \"Ridgeline Grid\",\n];\n\nconst companies = companyNames.map((name, i) => {\n  const sector = sectors[i % sectors.length];\n  return {\n    company: name,\n    sector: sector.name,\n    color: sector.color,\n    gx: i % 4,\n    gy: Math.floor(i / 4),\n    revenue_growth: lcg(),\n    employee_growth: lcg(),\n    profit_margin: lcg(),\n    liquidity_ratio: scaleRange(lcg(), 0.8, 3.2), // current-ratio style multiple\n    market_share: lcg(),\n    rd_intensity: scaleRange(lcg(), 1, 22), // % of revenue, realistic ceiling\n    debt_to_equity: scaleRange(lcg(), 0.1, 2.5), // multiple\n    customer_retention: scaleRange(lcg(), 60, 98), // %\n  };\n});\n\n// Min-max normalize each metric across all companies to [0, 1].\nconst metrics = [\n  \"revenue_growth\",\n  \"employee_growth\",\n  \"profit_margin\",\n  \"liquidity_ratio\",\n  \"market_share\",\n  \"rd_intensity\",\n  \"debt_to_equity\",\n  \"customer_retention\",\n];\nconst ranges = {};\nmetrics.forEach((m) => {\n  const values = companies.map((c) => c[m]);\n  ranges[m] = { min: Math.min(...values), max: Math.max(...values) };\n});\nfunction normalize(m, v) {\n  const { min, max } = ranges[m];\n  return max > min ? (v - min) / (max - min) : 0.5;\n}\n\n// Composite overall-profile score (simple average of growth/margin/share/\n// retention, offset by leverage) drives the single \"strongest profile\"\n// highlight drawn on the grid.\ncompanies.forEach((c) => {\n  c.compositeScore =\n    (normalize(\"revenue_growth\", c.revenue_growth) +\n      normalize(\"profit_margin\", c.profit_margin) +\n      normalize(\"market_share\", c.market_share) +\n      normalize(\"customer_retention\", c.customer_retention) +\n      (1 - normalize(\"debt_to_equity\", c.debt_to_equity))) /\n    5;\n});\nconst topPerformer = companies.reduce((best, c) =>\n  c.compositeScore > best.compositeScore ? c : best,\n);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chernoff-face drawing plugin --------------------------------------------\n// Chart.js positions each observation on an invisible scatter grid; this\n// plugin draws the actual face at each point's pixel location once the\n// dataset elements have been laid out.\nconst chernoffFacesPlugin = {\n  id: \"chernoffFaces\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const cellW = Math.abs(\n      scales.x.getPixelForValue(1) - scales.x.getPixelForValue(0),\n    );\n    const cellH = Math.abs(\n      scales.y.getPixelForValue(1) - scales.y.getPixelForValue(0),\n    );\n\n    chart.data.datasets.forEach((dataset, di) => {\n      if (!chart.isDatasetVisible(di)) return;\n      const meta = chart.getDatasetMeta(di);\n      dataset.data.forEach((raw, i) => {\n        const el = meta.data[i];\n        if (!el) return;\n        drawFace(\n          ctx,\n          el.x,\n          el.y,\n          cellW,\n          cellH,\n          raw,\n          raw.company === topPerformer.company,\n        );\n      });\n    });\n  },\n};\n\nfunction drawFace(ctx, cx, cy, cellW, cellH, r, isTopPerformer) {\n  const headRx =\n    cellW * 0.24 * (0.75 + 0.5 * normalize(\"revenue_growth\", r.revenue_growth));\n  const headRy =\n    cellH *\n    0.28 *\n    (0.75 + 0.5 * normalize(\"employee_growth\", r.employee_growth));\n  const eyeR =\n    headRx * (0.08 + 0.14 * normalize(\"profit_margin\", r.profit_margin));\n  const mouthCurve =\n    headRy * 0.55 * (2 * normalize(\"liquidity_ratio\", r.liquidity_ratio) - 1);\n  const browSlant = 10 * (2 * normalize(\"market_share\", r.market_share) - 1);\n  const noseLen =\n    headRy * (0.15 + 0.35 * normalize(\"rd_intensity\", r.rd_intensity));\n  const eyeSpacing =\n    headRx * (0.34 + 0.16 * normalize(\"debt_to_equity\", r.debt_to_equity));\n  const mouthWidth =\n    headRx *\n    (0.42 + 0.28 * normalize(\"customer_retention\", r.customer_retention));\n\n  ctx.save();\n\n  // Highlight ring: marks the company with the strongest overall profile\n  // (composite of growth, margin, market share, retention, and leverage).\n  if (isTopPerformer) {\n    ctx.beginPath();\n    ctx.ellipse(cx, cy, headRx * 1.28, headRy * 1.28, 0, 0, Math.PI * 2);\n    ctx.setLineDash([6, 4]);\n    ctx.lineWidth = 2;\n    ctx.strokeStyle = t.ink;\n    ctx.stroke();\n    ctx.setLineDash([]);\n  }\n\n  // Head\n  ctx.beginPath();\n  ctx.ellipse(cx, cy, headRx, headRy, 0, 0, Math.PI * 2);\n  ctx.fillStyle = t.pageBg;\n  ctx.fill();\n  ctx.lineWidth = 3;\n  ctx.strokeStyle = r.color;\n  ctx.stroke();\n\n  // Eyebrows (slant encodes market share)\n  const eyeOffsetX = eyeSpacing;\n  const eyeY = cy - headRy * 0.15;\n  ctx.strokeStyle = t.ink;\n  ctx.lineWidth = 2.5;\n  ctx.lineCap = \"round\";\n  [-1, 1].forEach((sign) => {\n    const bx = cx + sign * eyeOffsetX;\n    const by = eyeY - eyeR - headRy * 0.16;\n    ctx.beginPath();\n    ctx.moveTo(bx - headRx * 0.14, by + sign * browSlant * 0.35);\n    ctx.lineTo(bx + headRx * 0.14, by - sign * browSlant * 0.35);\n    ctx.stroke();\n  });\n\n  // Eyes (size encodes profit margin)\n  ctx.fillStyle = t.ink;\n  [-1, 1].forEach((sign) => {\n    ctx.beginPath();\n    ctx.arc(cx + sign * eyeOffsetX, eyeY, eyeR, 0, Math.PI * 2);\n    ctx.fill();\n  });\n\n  // Nose (length encodes R&D intensity)\n  ctx.beginPath();\n  ctx.moveTo(cx, cy - headRy * 0.02);\n  ctx.lineTo(cx, cy + noseLen);\n  ctx.strokeStyle = t.inkSoft;\n  ctx.lineWidth = 2;\n  ctx.stroke();\n\n  // Mouth (curvature encodes liquidity ratio, width encodes customer retention)\n  const mouthY = cy + headRy * 0.55;\n  const mouthW = mouthWidth;\n  ctx.beginPath();\n  ctx.moveTo(cx - mouthW, mouthY);\n  ctx.quadraticCurveTo(cx, mouthY + mouthCurve, cx + mouthW, mouthY);\n  ctx.strokeStyle = t.ink;\n  ctx.lineWidth = 2.5;\n  ctx.stroke();\n\n  ctx.restore();\n\n  // Label — the top-performer's name gets a bold \"★\" prefix to flag it as\n  // the standout face on the grid. Its baseline drops below the highlight\n  // ring (not just the head) so the dashed stroke never crosses the text.\n  ctx.save();\n  ctx.textAlign = \"center\";\n  if (isTopPerformer) {\n    ctx.fillStyle = t.ink;\n    ctx.font = \"bold 13px sans-serif\";\n    ctx.fillText(`★ ${r.company}`, cx, cy + headRy * 1.28 + 20);\n  } else {\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"13px sans-serif\";\n    ctx.fillText(r.company, cx, cy + headRy + 20);\n  }\n  ctx.restore();\n}\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: sectors.map((sector) => ({\n      label: sector.name,\n      data: companies\n        .filter((c) => c.sector === sector.name)\n        .map((c) => ({ x: c.gx, y: c.gy, ...c })),\n      backgroundColor: sector.color,\n      borderColor: sector.color,\n      pointStyle: \"circle\",\n      pointRadius: 0,\n      pointHitRadius: 55,\n      pointHoverRadius: 0,\n    })),\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 10, bottom: 10, left: 40, right: 40 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"chernoff-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: { color: t.ink, font: { size: 16 }, usePointStyle: true },\n      },\n      tooltip: {\n        callbacks: {\n          title: (items) => items[0].raw.company,\n          label: (item) => {\n            const r = item.raw;\n            return [\n              `Sector: ${r.sector}`,\n              `Revenue growth: ${(r.revenue_growth * 100).toFixed(0)}%`,\n              `Employee growth: ${(r.employee_growth * 100).toFixed(0)}%`,\n              `Profit margin: ${(r.profit_margin * 100).toFixed(0)}%`,\n              `Liquidity ratio: ${r.liquidity_ratio.toFixed(2)}x`,\n              `Market share: ${(r.market_share * 100).toFixed(0)}%`,\n              `R&D intensity: ${r.rd_intensity.toFixed(1)}% of revenue`,\n              `Debt-to-equity: ${r.debt_to_equity.toFixed(2)}x`,\n              `Customer retention: ${r.customer_retention.toFixed(0)}%`,\n            ];\n          },\n        },\n      },\n    },\n    scales: {\n      x: { display: false, min: -0.6, max: 3.6 },\n      y: { display: false, min: -0.6, max: 2.6, reverse: true },\n    },\n  },\n  plugins: [chernoffFacesPlugin],\n});\n"}