{"spec_id":"marimekko-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// marimekko-basic: Basic Marimekko Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-07-24\n//# anyplot-orientation: landscape\n// anyplot.ai\n// marimekko-basic: Basic Marimekko Chart\n// Library: echarts 5.5.1 | JavaScript 22\n// Quality: pending | Created: 2026-07-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data: quarterly revenue ($M) by region (column width) and product line\n// (stacked segment height) ---------------------------------------------------\nconst regions = [\n  { name: \"North America\", values: [420, 180, 260, 90] },\n  { name: \"Europe\", values: [310, 140, 200, 70] },\n  { name: \"Asia Pacific\", values: [260, 310, 150, 40] },\n  { name: \"Latin America\", values: [90, 60, 70, 20] },\n  { name: \"Middle East & Africa\", values: [60, 40, 50, 15] },\n];\nconst products = [\"Software\", \"Hardware\", \"Services\", \"Consulting\"];\n// Segment label text color: white reads well on the brand green and blue\n// slots, but the lighter lavender and ochre slots fall below the WCAG AA\n// large-text 3:1 minimum against white — those two get a fixed dark ink\n// label instead (the fill colors are identical across themes, so the fix\n// stays fixed rather than following t.ink).\nconst labelTextColor = [\"#FFFFFF\", \"#1A1A17\", \"#FFFFFF\", \"#1A1A17\"];\n\nconst regionTotals = regions.map((r) => r.values.reduce((a, b) => a + b, 0));\nconst grandTotal = regionTotals.reduce((a, b) => a + b, 0);\n\n// --- Layout geometry (CSS px within the mount) -------------------------------\nconst marginLeft = 110;\nconst marginRight = 50;\nconst marginTop = 160;\nconst marginBottom = 110;\nconst plotWidth = size.width - marginLeft - marginRight;\nconst plotHeight = size.height - marginTop - marginBottom;\nconst columnGap = 8;\n\nconst measureCtx = document.createElement(\"canvas\").getContext(\"2d\");\nfunction textWidth(text, font) {\n  measureCtx.font = font;\n  return measureCtx.measureText(text).width;\n}\n\n// --- Build segment rectangles + per-column metadata --------------------------\nconst rects = [];\nconst columns = [];\nlet xCursor = marginLeft;\n\nregions.forEach((region, ri) => {\n  const total = regionTotals[ri];\n  const columnWidth = (total / grandTotal) * plotWidth;\n  const innerWidth = Math.max(columnWidth - columnGap, 1);\n  let yCursor = marginTop + plotHeight;\n\n  region.values.forEach((value, pi) => {\n    const share = value / total;\n    const segHeight = share * plotHeight;\n    const rectY = yCursor - segHeight;\n    rects.push({\n      // [x, y, w, h, labelText, showLabel, labelColor, regionName, productName, value, share]\n      value: [\n        xCursor,\n        rectY,\n        innerWidth,\n        segHeight,\n        `$${value}M`,\n        segHeight > 58 ? 1 : 0,\n        labelTextColor[pi],\n        region.name,\n        products[pi],\n        value,\n        share,\n      ],\n      itemStyle: { color: t.palette[pi] },\n    });\n    yCursor = rectY;\n  });\n\n  columns.push({ x: xCursor + innerWidth / 2, name: region.name, total });\n  xCursor += columnWidth;\n});\n\n// --- Graphic overlay: left percentage scale, legend, column labels ----------\nconst graphicElements = [];\n\n// Vertical axis line + percentage ticks/labels\ngraphicElements.push({\n  type: \"line\",\n  shape: { x1: marginLeft, y1: marginTop, x2: marginLeft, y2: marginTop + plotHeight },\n  style: { stroke: t.inkSoft, lineWidth: 1.5 },\n  silent: true,\n});\n[0, 25, 50, 75, 100].forEach((pct) => {\n  const y = marginTop + plotHeight * (1 - pct / 100);\n  graphicElements.push({\n    type: \"line\",\n    shape: { x1: marginLeft - 8, y1: y, x2: marginLeft, y2: y },\n    style: { stroke: t.inkSoft, lineWidth: 1.5 },\n    silent: true,\n  });\n  graphicElements.push({\n    type: \"text\",\n    x: marginLeft - 14,\n    y,\n    style: { text: `${pct}%`, fill: t.inkSoft, fontSize: 14, align: \"right\", verticalAlign: \"middle\" },\n    silent: true,\n  });\n});\ngraphicElements.push({\n  type: \"text\",\n  x: 34,\n  y: marginTop + plotHeight / 2,\n  rotation: -Math.PI / 2,\n  style: { text: \"Share of Column Revenue\", fill: t.inkSoft, fontSize: 15, align: \"center\" },\n  silent: true,\n});\n\n// Column labels (region name, rotated to fit narrow columns) + total above bar\ncolumns.forEach((c) => {\n  graphicElements.push({\n    type: \"text\",\n    x: c.x,\n    y: marginTop + plotHeight + 16,\n    rotation: Math.PI / 7,\n    style: { text: c.name, fill: t.inkSoft, fontSize: 15, align: \"right\", verticalAlign: \"top\" },\n    silent: true,\n  });\n  graphicElements.push({\n    type: \"text\",\n    x: c.x,\n    y: marginTop - 18,\n    style: {\n      text: `$${c.total}M`,\n      fill: t.ink,\n      fontSize: 15,\n      fontWeight: \"bold\",\n      align: \"center\",\n      verticalAlign: \"bottom\",\n    },\n    silent: true,\n  });\n});\n\n// Manual legend (one swatch per stacked product line, above the plot area)\nconst legendFont = \"13px sans-serif\";\nconst swatchSize = 16;\nconst swatchGap = 8;\nconst itemGap = 36;\nconst legendItemWidths = products.map((name) => swatchSize + swatchGap + textWidth(name, legendFont) + itemGap);\nconst legendTotalWidth = legendItemWidths.reduce((a, b) => a + b, 0) - itemGap;\nlet legendX = size.width / 2 - legendTotalWidth / 2;\nconst legendY = 100;\n\nproducts.forEach((name, pi) => {\n  graphicElements.push({\n    type: \"rect\",\n    x: legendX,\n    y: legendY,\n    shape: { x: 0, y: 0, width: swatchSize, height: swatchSize, r: 3 },\n    style: { fill: t.palette[pi] },\n    silent: true,\n  });\n  graphicElements.push({\n    type: \"text\",\n    x: legendX + swatchSize + swatchGap,\n    y: legendY + swatchSize / 2,\n    style: { text: name, fill: t.ink, fontSize: 16, align: \"left\", verticalAlign: \"middle\" },\n    silent: true,\n  });\n  legendX += legendItemWidths[pi];\n});\n\n// --- Chart ------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  color: t.palette,\n  title: {\n    text: \"marimekko-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"medium\" },\n  },\n  graphic: graphicElements,\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    textStyle: { color: t.ink },\n    formatter: (params) => {\n      const [, , , , , , , regionName, productName, value, share] = params.value;\n      return `<b>${regionName} · ${productName}</b><br/>$${value}M &middot; ${(share * 100).toFixed(1)}% of column`;\n    },\n  },\n  series: [\n    {\n      type: \"custom\",\n      coordinateSystem: null,\n      clip: false,\n      renderItem: (params, api) => {\n        const x = api.value(0);\n        const y = api.value(1);\n        const w = api.value(2);\n        const h = api.value(3);\n        const labelText = api.value(4);\n        const showLabel = api.value(5) === 1;\n        const labelColor = api.value(6);\n        const children = [\n          {\n            type: \"rect\",\n            shape: { x, y, width: w, height: h },\n            style: api.style({ stroke: t.pageBg, lineWidth: 2 }),\n          },\n        ];\n        if (showLabel) {\n          children.push({\n            type: \"text\",\n            style: {\n              text: labelText,\n              x: x + w / 2,\n              y: y + h / 2,\n              fill: labelColor,\n              fontSize: 16,\n              fontWeight: \"bold\",\n              align: \"center\",\n              verticalAlign: \"middle\",\n            },\n          });\n        }\n        return { type: \"group\", children };\n      },\n      data: rects,\n    },\n  ],\n});\n"}