{"spec_id":"bar-heart-rate-zones","library":"d3","language":"javascript","code":"// anyplot.ai\n// bar-heart-rate-zones: Time in Heart Rate Zones Bar Chart\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-14\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst THEME = window.ANYPLOT_THEME;\n\n// Semantic zone colors — conventional fitness-zone hues mapped to Imprint palette\nconst muted = THEME === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\nconst zoneColors = [\n  muted,      // Z1 Recovery  — grey  (Imprint muted anchor)\n  \"#4467A3\",  // Z2 Endurance — blue  (Imprint pos 3)\n  \"#009E73\",  // Z3 Aerobic   — green (Imprint pos 1 / brand)\n  \"#BD8233\",  // Z4 Threshold — ochre (Imprint pos 4)\n  \"#AE3030\",  // Z5 Maximum   — matte red (Imprint pos 5)\n];\n\n// Data — 60-minute tempo run\nconst zones = [\n  { id: \"Z1\", name: \"Recovery\",  minutes: 8,  hrRange: \"< 50% max HR\"  },\n  { id: \"Z2\", name: \"Endurance\", minutes: 22, hrRange: \"50–60% max HR\" },\n  { id: \"Z3\", name: \"Aerobic\",   minutes: 15, hrRange: \"60–70% max HR\" },\n  { id: \"Z4\", name: \"Threshold\", minutes: 12, hrRange: \"70–80% max HR\" },\n  { id: \"Z5\", name: \"Maximum\",   minutes: 3,  hrRange: \"80%+ max HR\"   },\n];\n\nconst margin = { top: 90, right: 60, bottom: 130, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\nconst g = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Scales\nconst x = d3.scaleBand()\n  .domain(zones.map(d => d.id))\n  .range([0, iw])\n  .padding(0.32);\n\nconst y = d3.scaleLinear()\n  .domain([0, 30])\n  .range([ih, 0]);\n\n// Horizontal gridlines (y-axis only)\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickValues([5, 10, 15, 20, 25, 30]).tickSize(-iw).tickFormat(\"\"))\n  .call(ag => ag.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// Y axis with labels\ng.append(\"g\")\n  .call(\n    d3.axisLeft(y)\n      .tickValues([0, 5, 10, 15, 20, 25, 30])\n      .tickFormat(d => d === 0 ? \"\" : `${d} min`)\n  )\n  .call(ag => {\n    ag.select(\".domain\").attr(\"stroke\", t.inkSoft);\n    ag.selectAll(\".tick text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n    ag.selectAll(\".tick line\").remove();\n  });\n\n// X axis baseline\ng.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickSize(0).tickFormat(\"\"))\n  .call(ag => ag.select(\".domain\").attr(\"stroke\", t.inkSoft));\n\n// Bars\ng.selectAll(\".bar\")\n  .data(zones)\n  .join(\"rect\")\n  .attr(\"x\", d => x(d.id))\n  .attr(\"y\", d => y(d.minutes))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"height\", d => ih - y(d.minutes))\n  .attr(\"fill\", (d, i) => zoneColors[i])\n  .attr(\"rx\", 4);\n\n// Duration labels above each bar\ng.selectAll(\".dur-label\")\n  .data(zones)\n  .join(\"text\")\n  .attr(\"x\", d => x(d.id) + x.bandwidth() / 2)\n  .attr(\"y\", d => y(d.minutes) - 12)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .style(\"font-weight\", \"700\")\n  .text(d => {\n    const m = Math.floor(d.minutes);\n    const s = Math.round((d.minutes - m) * 60);\n    return `${m}:${String(s).padStart(2, \"0\")}`;\n  });\n\n// X-axis zone label groups: ID + name + HR range\nconst xLabels = g.selectAll(\".zone-label\")\n  .data(zones)\n  .join(\"g\")\n  .attr(\"transform\", d => `translate(${x(d.id) + x.bandwidth() / 2},${ih})`);\n\nxLabels.append(\"text\")\n  .attr(\"y\", 30)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .style(\"font-weight\", \"700\")\n  .text(d => d.id);\n\nxLabels.append(\"text\")\n  .attr(\"y\", 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(d => d.name);\n\nxLabels.append(\"text\")\n  .attr(\"y\", 76)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(d => d.hrRange);\n\n// Y-axis label (rotated)\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(ih / 2))\n  .attr(\"y\", -72)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Time (minutes)\");\n\n// Title\nconst titleText = \"60-min Tempo Run · bar-heart-rate-zones · javascript · d3 · anyplot.ai\";\nconst titleSize = Math.max(16, Math.round(22 * Math.min(1, 67 / titleText.length)));\n\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(titleText);\n"}