{"spec_id":"marimekko-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// marimekko-basic: Basic Marimekko Chart\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 90/100 | Created: 2026-07-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 260, bottom: 130, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Revenue ($M) by region (bar width) and product line (segment height share)\nconst yCategories = [\"Software\", \"Hardware\", \"Services\", \"Support\"];\nconst data = [\n  { x: \"North America\", values: { Software: 420, Hardware: 180, Services: 150, Support: 90 } },\n  { x: \"Europe\", values: { Software: 260, Hardware: 140, Services: 130, Support: 70 } },\n  { x: \"Asia Pacific\", values: { Software: 310, Hardware: 90, Services: 60, Support: 40 } },\n  { x: \"Latin America\", values: { Software: 90, Hardware: 40, Services: 30, Support: 20 } },\n];\n\n// Columns pre-sorted largest-to-smallest total revenue, reinforcing an\n// implicit left-to-right visual hierarchy.\nconst regionTotal = (d) => d3.sum(yCategories, (cat) => d.values[cat]);\nconst sortedData = [...data].sort((a, b) => regionTotal(b) - regionTotal(a));\n\n// --- d3-hierarchy layout: dice-then-slice treemap tiling ---------------------\n// The canonical D3 technique for a Marimekko/mosaic layout: build a two-level\n// hierarchy (region -> product line) and tile it with a custom function that\n// dices the root's children (columns, widths proportional to region totals)\n// then slices each column's children (segments, heights proportional to share).\n// Stacking order matches a conventional stacked bar: first category at the\n// bottom of each column, so product lines are fed to the slice in reverse.\nconst stackOrder = [...yCategories].reverse();\nconst root = d3\n  .hierarchy({\n    children: sortedData.map((d) => ({\n      name: d.x,\n      children: stackOrder.map((cat) => ({ name: cat, cat, value: d.values[cat] })),\n    })),\n  })\n  .sum((d) => d.value);\n\nconst gap = 10;\nfunction marimekkoTile(node, x0, y0, x1, y1) {\n  if (node.depth === 0) {\n    const n = node.children.length;\n    d3.treemapDice(node, x0, y0, x1 - gap * (n - 1), y1);\n    let shift = 0;\n    node.children.forEach((c, i) => {\n      c.x0 += shift;\n      c.x1 += shift;\n      if (i < n - 1) shift += gap;\n    });\n  } else {\n    d3.treemapSlice(node, x0, y0, x1, y1);\n  }\n}\n\nd3.treemap().tile(marimekkoTile).size([iw, ih])(root);\n\nconst columns = root.children; // depth-1 nodes: one per region\nconst leaves = root.leaves(); // depth-2 nodes: one per region/product-line segment\nconst color = d3.scaleOrdinal().domain(yCategories).range(t.palette);\n\n// Single largest segment across the whole dataset — the design flourish\n// called out for review: emphasize the key insight instead of a plain default.\nconst largestLeaf = leaves.reduce((a, b) => (b.value > a.value ? b : a));\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Y axis (share of column revenue) ------------------------------------------\nconst shareScale = d3.scaleLinear().domain([0, 1]).range([ih, 0]);\nconst yAxis = g.append(\"g\").call(d3.axisLeft(shareScale).ticks(4).tickFormat(d3.format(\".0%\")));\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nyAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Share of Regional Revenue\");\n\n// --- Columns (variable-width stacked bars) -------------------------------------\ng.selectAll(\"rect.segment\")\n  .data(leaves)\n  .join(\"rect\")\n  .attr(\"class\", \"segment\")\n  .attr(\"x\", (d) => d.x0)\n  .attr(\"y\", (d) => d.y0)\n  .attr(\"width\", (d) => d.x1 - d.x0)\n  .attr(\"height\", (d) => d.y1 - d.y0)\n  .attr(\"fill\", (d) => color(d.data.cat))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// --- Reference gridlines (quartile shares), drawn above the bars at low ------\n// opacity so they read as an intentional reference layer instead of dead code\n// hidden underneath the opaque columns.\ng.selectAll(\"line.grid\")\n  .data([0.25, 0.5, 0.75])\n  .join(\"line\")\n  .attr(\"class\", \"grid\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => shareScale(d))\n  .attr(\"y2\", (d) => shareScale(d))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"4,4\")\n  .attr(\"opacity\", 0.3);\n\n// Dashed outline calling out the single largest segment in the dataset.\ng.append(\"rect\")\n  .attr(\"x\", largestLeaf.x0)\n  .attr(\"y\", largestLeaf.y0)\n  .attr(\"width\", largestLeaf.x1 - largestLeaf.x0)\n  .attr(\"height\", largestLeaf.y1 - largestLeaf.y0)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 3)\n  .attr(\"stroke-dasharray\", \"8,4\");\n\n// Value labels on segments large enough to hold them legibly\nconst labeled = leaves.filter((d) => d.x1 - d.x0 > 140 && d.y1 - d.y0 > 90);\ng.selectAll(\"text.segment-label\")\n  .data(labeled)\n  .join(\"text\")\n  .attr(\"class\", \"segment-label\")\n  .attr(\"x\", (d) => (d.x0 + d.x1) / 2)\n  .attr(\"y\", (d) => (d.y0 + d.y1) / 2)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.pageBg)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => `$${d3.format(\",\")(d.value)}M`);\n\n// Callout badge for the largest segment, stacked above its value label.\ng.append(\"text\")\n  .attr(\"x\", (largestLeaf.x0 + largestLeaf.x1) / 2)\n  .attr(\"y\", (largestLeaf.y0 + largestLeaf.y1) / 2 - 26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.pageBg)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"700\")\n  .style(\"letter-spacing\", \"0.04em\")\n  .text(\"LARGEST SEGMENT\");\n\n// --- X-axis labels (region name + column total, centered under each bar) ------\nconst xLabels = g\n  .selectAll(\"g.x-label\")\n  .data(columns)\n  .join(\"g\")\n  .attr(\"transform\", (d) => `translate(${(d.x0 + d.x1) / 2},${ih})`);\n\nxLabels\n  .append(\"text\")\n  .attr(\"y\", 30)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d.data.name);\n\nxLabels\n  .append(\"text\")\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text((d) => `$${d3.format(\",\")(d.value)}M total`);\n\n// --- Legend ---------------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 50},${margin.top + 30})`);\n\nconst legendItems = legend\n  .selectAll(\"g.legend-item\")\n  .data(yCategories)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (d, i) => `translate(0,${i * 34})`);\n\nlegendItems\n  .append(\"rect\")\n  .attr(\"width\", 18)\n  .attr(\"height\", 18)\n  .attr(\"rx\", 2)\n  .attr(\"fill\", (d) => color(d));\n\nlegendItems\n  .append(\"text\")\n  .attr(\"x\", 26)\n  .attr(\"y\", 14)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .text((d) => d);\n\n// --- Title -----------------------------------------------------------------------\nsvg\n  .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\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"marimekko-basic · javascript · d3 · anyplot.ai\");\n"}