{"spec_id":"count-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// count-basic: Basic Count Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 94/100 | Created: 2026-08-11\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: raw categorical observations, counted client-side --------------\n// (mirrors a countplot / value_counts() workflow rather than pre-aggregated bars)\nfunction lcg(seed) {\n  let state = seed;\n  return function () {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst categories = [\n  \"Billing\",\n  \"Technical\",\n  \"Shipping\",\n  \"Account\",\n  \"Feature Request\",\n  \"Other\",\n];\nconst weights = [0.27, 0.24, 0.18, 0.15, 0.11, 0.05];\nconst cumulative = weights.reduce((acc, w) => {\n  acc.push((acc.length ? acc[acc.length - 1] : 0) + w);\n  return acc;\n}, []);\n\nconst ticketCount = 320;\nconst counts = new Map(categories.map((category) => [category, 0]));\nfor (let i = 0; i < ticketCount; i++) {\n  const r = rand();\n  const idx = cumulative.findIndex((c) => r <= c);\n  const category = categories[idx === -1 ? categories.length - 1 : idx];\n  counts.set(category, counts.get(category) + 1);\n}\n\nconst sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);\nconst labels = sorted.map(([label]) => label);\nconst values = sorted.map(([, value]) => value);\n\n// Pareto overlay: running share of all tickets covered as categories are\n// added left-to-right along the descending sort. This is the spec's own\n// \"optional percentage annotations\" note, expressed as a genuine Chart.js\n// mixed-type dataset (bar + line, dual y-axis) rather than a plain % label.\nlet runningTotal = 0;\nconst cumulativeShare = values.map((value) => {\n  runningTotal += value;\n  return (runningTotal / ticketCount) * 100;\n});\n\n// --- Mount ------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Distinctive Chart.js touches -------------------------------------------\n// Top-to-bottom color falloff on each bar (same brand hue in both themes, only\n// the gradient stop mix differs), a custom draw plugin that renders the exact\n// count above every bar, and a mixed bar+line dataset (dual y-axis) that\n// overlays the Pareto cumulative-share curve from the second Imprint color.\nfunction mixHex(hexA, hexB, weightA) {\n  const a = parseInt(hexA.slice(1), 16);\n  const b = parseInt(hexB.slice(1), 16);\n  const mix = (shift) => {\n    const va = (a >> shift) & 0xff;\n    const vb = (b >> shift) & 0xff;\n    return Math.round(va * weightA + vb * (1 - weightA));\n  };\n  const r = mix(16);\n  const g = mix(8);\n  const bl = mix(0);\n  return `#${((r << 16) | (g << 8) | bl).toString(16).padStart(6, \"0\")}`;\n}\n\n// Fully opaque bottom stop (blended toward the page background instead of\n// using alpha) so the line dataset drawn on top never composites with the\n// bar and shifts hue.\nfunction barGradient(context) {\n  const { chart } = context;\n  const { ctx, chartArea } = chart;\n  if (!chartArea) return t.palette[0];\n  const gradient = ctx.createLinearGradient(\n    0,\n    chartArea.top,\n    0,\n    chartArea.bottom,\n  );\n  gradient.addColorStop(0, t.palette[0]);\n  gradient.addColorStop(1, mixHex(t.palette[0], t.pageBg, 0.6));\n  return gradient;\n}\n\nconst countLabelPlugin = {\n  id: \"countLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    const meta = chart.getDatasetMeta(0);\n    const lineMeta = chart.getDatasetMeta(1);\n    ctx.save();\n    ctx.font = \"600 15px sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"bottom\";\n    // Halo stroke in the page background color keeps the label legible even\n    // where the cumulative-share line crosses behind it (data-dependent, so\n    // the crossing point can't be avoided by fixed positioning alone). When\n    // the line point lands close to the default offset, push the label\n    // further up so it clears the dashed stroke entirely.\n    ctx.strokeStyle = t.pageBg;\n    ctx.lineWidth = 4;\n    ctx.lineJoin = \"round\";\n    ctx.fillStyle = t.ink;\n    meta.data.forEach((bar, i) => {\n      const text = String(chart.data.datasets[0].data[i]);\n      const linePoint = lineMeta.data[i];\n      const nearLine = linePoint && Math.abs(linePoint.y - (bar.y - 8)) < 14;\n      const yOffset = nearLine ? 20 : 8;\n      ctx.strokeText(text, bar.x, bar.y - yOffset);\n      ctx.fillText(text, bar.x, bar.y - yOffset);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart --------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels,\n    datasets: [\n      {\n        type: \"bar\",\n        label: \"Support Tickets\",\n        data: values,\n        backgroundColor: barGradient,\n        borderRadius: 4,\n        maxBarThickness: 140,\n        categoryPercentage: 0.85,\n        barPercentage: 0.9,\n        yAxisID: \"count\",\n        order: 2,\n      },\n      {\n        type: \"line\",\n        label: \"Cumulative Share\",\n        data: cumulativeShare,\n        borderColor: t.palette[1],\n        backgroundColor: t.palette[1],\n        borderWidth: 2,\n        borderDash: [6, 4],\n        pointRadius: 6,\n        pointBackgroundColor: t.palette[1],\n        tension: 0.25,\n        yAxisID: \"percentage\",\n        order: 1,\n      },\n    ],\n  },\n  plugins: [countLabelPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 40, right: 10, bottom: 10, left: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"count-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 6 },\n      },\n      subtitle: {\n        display: true,\n        text: `n = ${ticketCount} tickets`,\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 24 },\n      },\n      legend: {\n        display: true,\n        labels: { color: t.ink, font: { size: 14 }, boxWidth: 22 },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: \"Support Ticket Category\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      count: {\n        type: \"linear\",\n        position: \"left\",\n        beginAtZero: true,\n        ticks: { color: t.inkSoft, font: { size: 14 }, precision: 0 },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Count\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      percentage: {\n        type: \"linear\",\n        position: \"right\",\n        min: 0,\n        max: 100,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => `${value}%`,\n        },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: \"Cumulative Share\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n    },\n  },\n});\n"}