{"spec_id":"learning-curve-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// learning-curve-basic: Model Learning Curve\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Simulated 10-fold cross-validation learning curve for a random-forest\n// churn classifier: accuracy vs. training set size.\nlet lcgState = 42;\nfunction lcgRandom() {\n  lcgState = (lcgState * 1664525 + 1013904223) % 4294967296;\n  return lcgState / 4294967296;\n}\nfunction gaussianNoise(std) {\n  const u1 = lcgRandom() || 1e-9;\n  const u2 = lcgRandom();\n  return std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst trainSizes = [200, 400, 800, 1200, 1600, 2000, 2400, 2800, 3200];\nconst foldCount = 10;\n\n// Training accuracy starts near-perfect and eases down as the model sees\n// more (harder) examples; validation accuracy starts low and climbs toward\n// the training curve — the classic converging-gap shape of a well-fit model.\nconst trainMean = trainSizes.map((n) => 0.975 - 0.05 * (1 - Math.exp(-n / 900)));\nconst validationMean = trainSizes.map((n) => 0.72 + 0.205 * (1 - Math.exp(-n / 1100)));\n\nfunction simulateFolds(meanCurve, baseStd, decayScale) {\n  const folds = [];\n  for (let f = 0; f < foldCount; f += 1) {\n    folds.push(\n      meanCurve.map((mean, i) => {\n        const std = baseStd * Math.exp(-trainSizes[i] / decayScale) + baseStd * 0.25;\n        return Math.min(1, Math.max(0, mean + gaussianNoise(std)));\n      })\n    );\n  }\n  return folds;\n}\n\n// Validation folds carry more spread than training folds, especially at\n// small sample sizes — the usual high-variance signature of held-out data.\nconst trainFolds = simulateFolds(trainMean, 0.02, 1800);\nconst validationFolds = simulateFolds(validationMean, 0.06, 1400);\n\nfunction meanAndStd(folds) {\n  const sizeCount = folds[0].length;\n  const mean = [];\n  const std = [];\n  for (let i = 0; i < sizeCount; i += 1) {\n    const column = folds.map((row) => row[i]);\n    const m = column.reduce((a, b) => a + b, 0) / column.length;\n    const variance = column.reduce((a, b) => a + (b - m) ** 2, 0) / column.length;\n    mean.push(m);\n    std.push(Math.sqrt(variance));\n  }\n  return { mean, std };\n}\n\nconst train = meanAndStd(trainFolds);\nconst validation = meanAndStd(validationFolds);\n\n// Remaining generalization gap at the largest training set size — annotated\n// below with a markLine connecting the two final points.\nconst lastIndex = trainSizes.length - 1;\nconst finalGap = train.mean[lastIndex] - validation.mean[lastIndex];\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nconst trainColor = t.palette[0]; // brand green — always first series\nconst validationColor = t.palette[1]; // lavender — second categorical series\n\n// A ±1 std confidence band, drawn with the standard ECharts stacked-area\n// trick: an invisible line at (mean - std), then a filled band of height\n// (2 * std) stacked on top of it, so the visible area spans mean ± std.\nfunction confidenceBand(name, mean, std, color) {\n  const lowerBound = mean.map((m, i) => m - std[i]);\n  const bandHeight = mean.map((_, i) => 2 * std[i]);\n  return [\n    {\n      name: `${name} lower bound`,\n      type: \"line\",\n      data: lowerBound,\n      stack: `${name}-band`,\n      symbol: \"none\",\n      lineStyle: { opacity: 0 },\n      areaStyle: { opacity: 0 },\n      silent: true,\n      tooltip: { show: false },\n    },\n    {\n      name: `${name} band`,\n      type: \"line\",\n      data: bandHeight,\n      stack: `${name}-band`,\n      symbol: \"none\",\n      lineStyle: { opacity: 0 },\n      areaStyle: { color, opacity: 0.15 },\n      silent: true,\n      tooltip: { show: false },\n    },\n  ];\n}\n\nchart.setOption({\n  animation: false,\n  color: [trainColor, validationColor],\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"learning-curve-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  legend: {\n    data: [\"Training score\", \"Validation score\"],\n    top: 56,\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  tooltip: { trigger: \"axis\" },\n  grid: { left: 100, right: 60, top: 130, bottom: 90 },\n  xAxis: {\n    type: \"category\",\n    data: trainSizes,\n    name: \"Training Set Size (samples)\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Accuracy\",\n    min: 0.6,\n    max: 1.0,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\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    ...confidenceBand(\"Training score\", train.mean, train.std, trainColor),\n    {\n      name: \"Training score\",\n      type: \"line\",\n      data: train.mean,\n      symbol: \"circle\",\n      symbolSize: 10,\n      lineStyle: { width: 3.5, color: trainColor },\n      itemStyle: { color: trainColor },\n    },\n    ...confidenceBand(\"Validation score\", validation.mean, validation.std, validationColor),\n    {\n      name: \"Validation score\",\n      type: \"line\",\n      data: validation.mean,\n      symbol: \"circle\",\n      symbolSize: 10,\n      lineStyle: { width: 3.5, color: validationColor },\n      itemStyle: { color: validationColor },\n      // Connects the final training/validation points with a labeled\n      // markLine calling out the residual generalization gap — a distinctive\n      // use of ECharts' arbitrary-coordinate markLine feature.\n      markLine: {\n        symbol: [\"none\", \"none\"],\n        silent: true,\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n        label: {\n          formatter: `Gap: ${finalGap.toFixed(3)}`,\n          color: t.inkSoft,\n          fontSize: 13,\n          position: \"middle\",\n        },\n        data: [\n          [\n            { coord: [lastIndex, train.mean[lastIndex]] },\n            { coord: [lastIndex, validation.mean[lastIndex]] },\n          ],\n        ],\n      },\n    },\n  ],\n});\n"}