{"spec_id":"scatter-regression-polynomial","library":"echarts","language":"javascript","code":"// anyplot.ai\n// scatter-regression-polynomial: Scatter Plot with Polynomial Regression\n// Library: echarts 6.1.0 | JavaScript 22.23.1\n// Quality: 96/100 | Created: 2026-08-11\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (deterministic, in-memory) ----------------------------------------\n// Crop yield response to nitrogen fertilizer — yield climbs with applied\n// nitrogen up to an agronomic optimum, then falls off as over-fertilization\n// harms the crop. A textbook diminishing/negative-returns curve, best\n// captured by a degree-2 polynomial rather than a straight line.\nlet seed = 42;\nfunction nextRandom() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction gaussianNoise(std) {\n  const u1 = Math.max(nextRandom(), 1e-9);\n  const u2 = nextRandom();\n  return std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst N_POINTS = 90;\nconst PEAK_X = 115; // kg/hectare — agronomic optimum nitrogen rate\nconst PEAK_Y = 9.2; // tons/hectare — yield at the optimum\nconst CURVATURE = -0.00023;\n\nconst nitrogen = Array.from({ length: N_POINTS }, () => 10 + nextRandom() * 210);\nconst cropYield = nitrogen.map(\n  (x) => PEAK_Y + CURVATURE * (x - PEAK_X) ** 2 + gaussianNoise(0.55)\n);\n\n// --- Quadratic regression (least squares via normal equations) -------------\nfunction fitDegree2(xs, ys) {\n  let s1 = 0, s2 = 0, s3 = 0, s4 = 0, sy = 0, sxy = 0, sx2y = 0;\n  for (let i = 0; i < xs.length; i++) {\n    const x = xs[i], y = ys[i], x2 = x * x;\n    s1 += x; s2 += x2; s3 += x2 * x; s4 += x2 * x2;\n    sy += y; sxy += x * y; sx2y += x2 * y;\n  }\n  const A = [\n    [xs.length, s1, s2],\n    [s1, s2, s3],\n    [s2, s3, s4],\n  ];\n  const rhs = [sy, sxy, sx2y];\n  const det3 = (m) =>\n    m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) -\n    m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) +\n    m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);\n  const withColumn = (col) => A.map((row, i) => row.map((v, j) => (j === col ? rhs[i] : v)));\n  const det = det3(A);\n  return [det3(withColumn(0)) / det, det3(withColumn(1)) / det, det3(withColumn(2)) / det];\n}\n\nconst [coefC, coefB, coefA] = fitDegree2(nitrogen, cropYield); // y = coefC + coefB*x + coefA*x^2\nconst predict = (x) => coefC + coefB * x + coefA * x * x;\n\nconst peakX = -coefB / (2 * coefA); // vertex of the fitted parabola (optimum nitrogen rate)\nconst peakY = predict(peakX);\n\nconst yMean = cropYield.reduce((s, y) => s + y, 0) / cropYield.length;\nconst ssRes = cropYield.reduce((s, y, i) => s + (y - predict(nitrogen[i])) ** 2, 0);\nconst ssTot = cropYield.reduce((s, y) => s + (y - yMean) ** 2, 0);\nconst rSquared = 1 - ssRes / ssTot;\nconst residualStd = Math.sqrt(ssRes / (cropYield.length - 3));\nconst bandHalfWidth = 1.5 * residualStd;\n\nconst xMin = Math.min(...nitrogen);\nconst xMax = Math.max(...nitrogen);\nconst CURVE_POINTS = 80;\nconst curveX = Array.from(\n  { length: CURVE_POINTS },\n  (_, i) => xMin + ((xMax - xMin) * i) / (CURVE_POINTS - 1)\n);\nconst curveData = curveX.map((x) => [x, predict(x)]);\nconst bandLowerData = curveX.map((x) => [x, predict(x) - bandHalfWidth]);\nconst bandWidthData = curveX.map((x) => [x, 2 * bandHalfWidth]);\n\nconst eqSign = (v) => (v >= 0 ? \"+ \" : \"− \");\nconst equationText =\n  \"y = \" + coefA.toFixed(5) + \"x² \" +\n  eqSign(coefB) + Math.abs(coefB).toFixed(3) + \"x \" +\n  eqSign(coefC) + Math.abs(coefC).toFixed(2);\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"scatter-regression-polynomial · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 22,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"bold\" },\n  },\n  legend: {\n    data: [\"Fertilizer trial plots\", \"±1.5σ band\", \"Quadratic fit (degree 2)\"],\n    top: 64,\n    left: \"center\",\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    itemWidth: 22,\n    itemHeight: 14,\n  },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    textStyle: { color: t.ink, fontSize: 13 },\n    formatter: function (params) {\n      if (params.seriesType === \"scatter\") {\n        return (\n          \"Nitrogen: \" + params.data[0].toFixed(1) + \" kg/ha<br>\" +\n          \"Yield: \" + params.data[1].toFixed(2) + \" t/ha\"\n        );\n      }\n      return params.seriesName;\n    },\n  },\n  grid: { left: 95, right: 240, top: 118, bottom: 108 },\n  xAxis: {\n    type: \"value\",\n    name: \"Nitrogen Applied (kg/hectare)\",\n    nameLocation: \"middle\",\n    nameGap: 42,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    scale: true,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Crop Yield (tons/hectare)\",\n    nameLocation: \"middle\",\n    nameGap: 56,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    scale: true,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Band lower bound\",\n      type: \"line\",\n      stack: \"confidence-band\",\n      symbol: \"none\",\n      lineStyle: { opacity: 0 },\n      areaStyle: { opacity: 0 },\n      data: bandLowerData,\n      silent: true,\n      tooltip: { show: false },\n      z: 1,\n    },\n    {\n      name: \"±1.5σ band\",\n      type: \"line\",\n      stack: \"confidence-band\",\n      symbol: \"none\",\n      lineStyle: { opacity: 0 },\n      itemStyle: { color: t.palette[1], opacity: 0.15 },\n      areaStyle: { color: t.palette[1], opacity: 0.15 },\n      data: bandWidthData,\n      silent: true,\n      tooltip: { show: false },\n      z: 1,\n    },\n    {\n      name: \"Fertilizer trial plots\",\n      type: \"scatter\",\n      symbolSize: 13,\n      data: nitrogen.map((x, i) => [x, cropYield[i]]),\n      itemStyle: {\n        color: t.palette[0],\n        opacity: 0.65,\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n      },\n      z: 5,\n    },\n    {\n      name: \"Quadratic fit (degree 2)\",\n      type: \"line\",\n      symbol: \"none\",\n      smooth: false,\n      data: curveData,\n      lineStyle: { color: t.palette[1], width: 4 },\n      itemStyle: { color: t.palette[1] },\n      z: 10,\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        label: {\n          formatter: \"Optimum: \" + peakX.toFixed(0) + \" kg/ha\",\n          color: t.inkSoft,\n          fontSize: 12,\n          position: \"insideEndTop\",\n        },\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n        data: [{ xAxis: peakX }],\n      },\n      markPoint: {\n        silent: true,\n        symbol: \"circle\",\n        symbolSize: 9,\n        itemStyle: { color: t.palette[1], borderColor: t.pageBg, borderWidth: 2 },\n        label: { show: false },\n        data: [{ coord: [peakX, peakY], name: \"Peak\" }],\n      },\n    },\n  ],\n  graphic: [\n    {\n      type: \"group\",\n      x: 1374,\n      y: 130,\n      children: [\n        {\n          type: \"rect\",\n          shape: { x: 0, y: 0, width: 210, height: 150, r: 5 },\n          style: { fill: t.elevatedBg, stroke: t.inkSoft, lineWidth: 0.8 },\n        },\n        {\n          type: \"text\",\n          x: 14,\n          y: 18,\n          style: { text: \"Quadratic Regression\", fill: t.ink, fontSize: 13, fontWeight: \"bold\" },\n        },\n        {\n          type: \"text\",\n          x: 14,\n          y: 46,\n          style: {\n            text: \"R² = \" + rSquared.toFixed(3),\n            fill: t.ink,\n            fontSize: 20,\n            fontWeight: \"bold\",\n          },\n        },\n        {\n          type: \"text\",\n          x: 14,\n          y: 82,\n          style: { text: equationText, fill: t.inkSoft, fontSize: 11 },\n        },\n        {\n          type: \"text\",\n          x: 14,\n          y: 108,\n          style: { text: \"n = \" + N_POINTS + \" plots\", fill: t.inkSoft, fontSize: 11 },\n        },\n      ],\n    },\n  ],\n});\n"}