{"spec_id":"chernoff-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// chernoff-basic: Chernoff Faces for Multivariate Data\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n};\nconst between = (lo, hi) => lo + rand() * (hi - lo);\n\nconst sectors = [\"Tech\", \"Retail\", \"Finance\"];\nconst names = [\n  [\"Nova Systems\", \"Vertex Cloud\", \"Pulsewave Robotics\", \"Arclight AI\"],\n  [\"Meridian Mart\", \"Cascade Outfitters\", \"Harborline Goods\", \"Willow & Oak\"],\n  [\"Anchor Capital\", \"Beacon Trust\", \"Silverline Bank\", \"Compass Holdings\"],\n];\n\nconst rawMetrics = [\n  \"revenueGrowth\", \"profitMargin\", \"liquidityRatio\", \"debtToEquity\",\n  \"marketShare\", \"rdInvestment\", \"customerSatisfaction\", \"employeeRetention\",\n];\nconst ranges = {\n  revenueGrowth: [-5, 25],\n  profitMargin: [-10, 30],\n  liquidityRatio: [0.5, 3.0],\n  debtToEquity: [0.2, 2.5],\n  marketShare: [1, 35],\n  rdInvestment: [1, 15],\n  customerSatisfaction: [40, 95],\n  employeeRetention: [60, 98],\n};\n\nconst rows = [];\nfor (let s = 0; s < sectors.length; s++) {\n  for (let i = 0; i < names[s].length; i++) {\n    const row = { name: names[s][i], sector: sectors[s], sectorIndex: s };\n    rawMetrics.forEach((m) => {\n      row[m] = between(ranges[m][0], ranges[m][1]);\n    });\n    rows.push(row);\n  }\n}\n\n// Normalize each metric to 0-1 across companies (per spec: common scale before mapping)\nrawMetrics.forEach((m) => {\n  const vals = rows.map((r) => r[m]);\n  const lo = Math.min(...vals);\n  const hi = Math.max(...vals);\n  rows.forEach((r) => {\n    r[m + \"Norm\"] = (r[m] - lo) / (hi - lo);\n  });\n});\n\n// --- Grid layout ---------------------------------------------------------------\nconst cols = 4;\nconst gridRows = 3;\nconst marginTop = size.height * 0.17;\nconst marginBottom = size.height * 0.06;\nconst marginLeft = size.width * 0.05;\nconst marginRight = size.width * 0.05;\nconst cellW = (size.width - marginLeft - marginRight) / cols;\nconst cellH = (size.height - marginTop - marginBottom) / gridRows;\nconst baseR = Math.min(cellW, cellH) * 0.3;\n\nconst sectorColors = [t.palette[0], t.palette[1], t.palette[2]];\n\n// Each observation: grid position + feature fractions (0-1 metric -> shape fraction)\nconst companies = rows.map((r, i) => {\n  const col = i % cols;\n  const gridRow = Math.floor(i / cols);\n  return {\n    name: r.name,\n    color: sectorColors[r.sectorIndex],\n    cx: marginLeft + cellW * (col + 0.5),\n    cy: marginTop + cellH * (gridRow + 0.5) - cellH * 0.08,\n    widthScale: 0.8 + 0.35 * r.revenueGrowthNorm, // face width <- revenue growth\n    heightScale: 0.8 + 0.35 * r.profitMarginNorm, // face height <- profit margin\n    eyeSizeFrac: 0.09 + 0.11 * r.marketShareNorm, // eye size <- market share\n    eyeSpacingFrac: 0.28 + 0.22 * r.customerSatisfactionNorm, // eye spacing <- customer satisfaction\n    browSlantFrac: 0.06 + 0.3 * r.debtToEquityNorm, // eyebrow slant <- debt-to-equity\n    noseLengthFrac: 0.15 + 0.28 * r.rdInvestmentNorm, // nose length <- R&D investment\n    mouthWidthFrac: 0.35 + 0.35 * r.liquidityRatioNorm, // mouth width <- liquidity ratio\n    curvatureFrac: r.employeeRetentionNorm - 0.5, // mouth curvature <- employee retention\n  };\n});\n\n// --- Face renderer ---------------------------------------------------------------\nconst renderItem = (params, api) => {\n  const c = companies[params.dataIndex];\n  const [px, py] = api.coord([api.value(0), api.value(1)]);\n  const faceRx = baseR * c.widthScale;\n  const faceRy = baseR * c.heightScale;\n  const eyeSize = faceRx * c.eyeSizeFrac;\n  const eyeSpacingHalf = faceRx * c.eyeSpacingFrac;\n  const eyeY = -faceRy * 0.15;\n  const browY = eyeY - eyeSize - faceRy * 0.05;\n  const browSlant = faceRy * c.browSlantFrac;\n  const browHalfLen = eyeSize * 1.3;\n  const noseTopY = eyeY + eyeSize * 0.5;\n  const noseLength = faceRy * c.noseLengthFrac;\n  const noseBottomY = noseTopY + noseLength;\n  const mouthY = faceRy * 0.48;\n  const mouthHalfWidth = faceRx * c.mouthWidthFrac;\n  const curvature = c.curvatureFrac * faceRy * 0.5;\n\n  return {\n    type: \"group\",\n    x: px,\n    y: py,\n    children: [\n      // face outline (width/height fractions above become an ellipse via scaleY)\n      {\n        type: \"circle\",\n        shape: { cx: 0, cy: 0, r: faceRx },\n        scaleY: faceRy / faceRx,\n        style: { fill: t.elevatedBg, stroke: c.color, lineWidth: 3 },\n      },\n      // eyes\n      { type: \"circle\", shape: { cx: -eyeSpacingHalf, cy: eyeY, r: eyeSize }, style: { fill: t.ink } },\n      { type: \"circle\", shape: { cx: eyeSpacingHalf, cy: eyeY, r: eyeSize }, style: { fill: t.ink } },\n      // eyebrows (inner point lower = steeper slant = higher debt-to-equity)\n      {\n        type: \"line\",\n        shape: {\n          x1: -eyeSpacingHalf - browHalfLen * 0.5, y1: browY - browSlant / 2,\n          x2: -eyeSpacingHalf + browHalfLen * 0.5, y2: browY + browSlant / 2,\n        },\n        style: { stroke: t.ink, lineWidth: 3 },\n      },\n      {\n        type: \"line\",\n        shape: {\n          x1: eyeSpacingHalf + browHalfLen * 0.5, y1: browY - browSlant / 2,\n          x2: eyeSpacingHalf - browHalfLen * 0.5, y2: browY + browSlant / 2,\n        },\n        style: { stroke: t.ink, lineWidth: 3 },\n      },\n      // nose\n      {\n        type: \"polyline\",\n        shape: { points: [[0, noseTopY], [0, noseBottomY], [7, noseBottomY]] },\n        style: { stroke: t.ink, lineWidth: 2, fill: \"none\" },\n      },\n      // mouth (positive curvature = corners pulled up = smile)\n      {\n        type: \"bezierCurve\",\n        shape: {\n          x1: -mouthHalfWidth, y1: mouthY, x2: mouthHalfWidth, y2: mouthY,\n          cpx1: -mouthHalfWidth * 0.5, cpy1: mouthY + curvature,\n          cpx2: mouthHalfWidth * 0.5, cpy2: mouthY + curvature,\n        },\n        style: { stroke: t.ink, lineWidth: 3, fill: \"none\" },\n      },\n      // label\n      {\n        type: \"text\",\n        style: {\n          text: c.name, x: 0, y: faceRy + 16,\n          fill: t.ink, fontSize: 13, fontWeight: 500,\n          align: \"center\", verticalAlign: \"top\",\n        },\n      },\n    ],\n  };\n};\n\n// --- Sector legend (static color key, top-right) ---------------------------------\nconst legend = sectors.map((name, i) => ({\n  type: \"group\",\n  x: size.width - marginRight - 150,\n  y: 20 + i * 24,\n  children: [\n    { type: \"circle\", shape: { cx: 0, cy: 0, r: 7 }, style: { fill: sectorColors[i] } },\n    {\n      type: \"text\",\n      style: {\n        text: `${name} sector`, x: 14, y: 0,\n        fill: t.inkSoft, fontSize: 13, align: \"left\", verticalAlign: \"middle\",\n      },\n    },\n  ],\n}));\n\n// --- Chart -------------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"chernoff-basic · javascript · echarts · anyplot.ai\",\n    subtext: \"Each face encodes 8 normalized financial-health metrics via facial features, grouped by sector\",\n    left: \"center\",\n    top: 14,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"bold\" },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  graphic: legend,\n  grid: { left: 0, right: 0, top: 0, bottom: 0 },\n  xAxis: { type: \"value\", show: false, min: 0, max: size.width },\n  yAxis: { type: \"value\", show: false, min: 0, max: size.height, inverse: true },\n  series: [\n    {\n      type: \"custom\",\n      renderItem,\n      data: companies.map((c) => [c.cx, c.cy]),\n    },\n  ],\n});\n"}