{"spec_id":"marimekko-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// marimekko-basic: Basic Marimekko Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 90/100 | Created: 2026-07-24\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic): revenue ($M) by region and product line ---\nconst products = [\"Cloud Services\", \"Software Licenses\", \"Hardware\", \"Professional Services\"];\nconst regions = [\n    { name: \"North America\", values: [420, 310, 180, 140] },\n    { name: \"Europe\", values: [260, 240, 150, 110] },\n    { name: \"Asia Pacific\", values: [300, 150, 220, 80] },\n    { name: \"Latin America\", values: [90, 70, 60, 40] },\n    { name: \"Middle East & Africa\", values: [60, 45, 50, 25] },\n];\n\n// --- Derived geometry ----------------------------------------------------------\n// Column width  ∝ total revenue of the region (x-axis units = $M, linear scale).\n// Segment height ∝ each product's share of that region's revenue (y-axis = %).\nconst colTotals = regions.map((r) => r.values.reduce((a, b) => a + b, 0));\nconst totalWidth = colTotals.reduce((a, b) => a + b, 0);\n\nlet cursor = 0;\nconst colStart = colTotals.map((width) => {\n    const start = cursor;\n    cursor += width;\n    return start;\n});\nconst midpoints = colStart.map((start, i) => start + colTotals[i] / 2);\nconst tickInfo = {};\nregions.forEach((r, i) => (tickInfo[midpoints[i]] = { name: r.name, dataWidth: colTotals[i] }));\n\n// One rectangle per (region, product) cell, plus an invisible point at its\n// center to drive the real tooltip — both derived from the same source data.\nconst segments = [];\nconst tooltipPoints = [];\nregions.forEach((region, i) => {\n    let shareBottom = 0;\n    region.values.forEach((value, j) => {\n        const share = (value / colTotals[i]) * 100;\n        segments.push({\n            x1: colStart[i],\n            x2: colStart[i] + colTotals[i],\n            yBottom: shareBottom,\n            yTop: shareBottom + share,\n            value,\n            share,\n            color: t.palette[j],\n            product: products[j],\n            region: region.name,\n        });\n        tooltipPoints.push({\n            x: midpoints[i],\n            y: shareBottom + share / 2,\n            custom: { region: region.name, product: products[j], value, share },\n        });\n        shareBottom += share;\n    });\n});\n\n// Perceived luminance decides whether a segment label reads better in white or ink.\nfunction labelColorFor(hex) {\n    const r = parseInt(hex.slice(1, 3), 16) / 255;\n    const g = parseInt(hex.slice(3, 5), 16) / 255;\n    const b = parseInt(hex.slice(5, 7), 16) / 255;\n    const luminance = 0.299 * r + 0.587 * g + 0.114 * b;\n    return luminance > 0.55 ? \"#1A1A17\" : \"#FFFFFF\";\n}\n\n// --- Chart -----------------------------------------------------------------\n// Core Highcharts has no variable-width column series (that lives in the\n// variwide module, which isn't loaded) — segments are drawn as plain rects via\n// the renderer, positioned with axis.toPixels() so they land exactly on the\n// linear width/percent axes below. Legend swatches and the tooltip come from\n// real (near-empty) series so hover still works in the exported HTML.\nHighcharts.chart(\"container\", {\n    chart: {\n        type: \"column\",\n        backgroundColor: \"transparent\",\n        animation: false,\n        style: { fontFamily: \"inherit\" },\n        events: {\n            load: function () {\n                const xAxis = this.xAxis[0];\n                const yAxis = this.yAxis[0];\n                const renderer = this.renderer;\n\n                segments.forEach((seg) => {\n                    const px1 = xAxis.toPixels(seg.x1);\n                    const px2 = xAxis.toPixels(seg.x2);\n                    const pyTop = yAxis.toPixels(seg.yTop);\n                    const pyBottom = yAxis.toPixels(seg.yBottom);\n                    const rectX = px1 + 1;\n                    const rectWidth = Math.max(px2 - px1 - 2, 0);\n                    const rectHeight = pyBottom - pyTop;\n\n                    renderer\n                        .rect(rectX, pyTop, rectWidth, rectHeight)\n                        .attr({ fill: seg.color, stroke: t.pageBg, \"stroke-width\": 2, zIndex: 3 })\n                        .add();\n\n                    if (rectWidth >= 70 && rectHeight >= 45) {\n                        renderer\n                            .text(`$${seg.value}M`, rectX + rectWidth / 2, pyTop + rectHeight / 2 + 5)\n                            .attr({ align: \"center\", zIndex: 4 })\n                            .css({ color: labelColorFor(seg.color), fontSize: \"15px\", fontWeight: \"600\" })\n                            .add();\n                    }\n                });\n            },\n        },\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n        text: \"marimekko-basic · javascript · highcharts · anyplot.ai\",\n        style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n    },\n    xAxis: {\n        min: 0,\n        max: totalWidth,\n        tickPositions: midpoints,\n        tickLength: 0,\n        gridLineWidth: 0,\n        lineColor: t.inkSoft,\n        labels: {\n            useHTML: true,\n            style: { color: t.inkSoft, fontSize: \"14px\" },\n            // Wrap each region name to its own column's pixel width so narrow\n            // columns (e.g. Middle East & Africa) break onto two lines instead\n            // of overlapping their neighbor's label.\n            formatter: function () {\n                const info = tickInfo[this.value];\n                if (!info) return \"\";\n                const axis = this.axis;\n                const pxWidth = Math.max(\n                    axis.toPixels(this.value + info.dataWidth / 2) - axis.toPixels(this.value - info.dataWidth / 2),\n                    40\n                );\n                return `<div style=\"width:${pxWidth}px; text-align:center; white-space:normal; line-height:1.25;\">${info.name}</div>`;\n            },\n        },\n        title: {\n            text: \"Region — column width ∝ total regional revenue\",\n            style: { color: t.inkSoft, fontSize: \"16px\" },\n        },\n    },\n    yAxis: {\n        min: 0,\n        max: 100,\n        tickInterval: 25,\n        gridLineColor: t.grid,\n        lineColor: t.inkSoft,\n        title: {\n            text: \"Share of Regional Revenue (%)\",\n            style: { color: t.inkSoft, fontSize: \"16px\" },\n        },\n        labels: {\n            style: { color: t.inkSoft, fontSize: \"14px\" },\n            formatter: function () {\n                return this.value + \"%\";\n            },\n        },\n    },\n    legend: {\n        itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n        itemHoverStyle: { color: t.ink },\n    },\n    tooltip: {\n        backgroundColor: t.elevatedBg,\n        borderColor: t.grid,\n        style: { color: t.ink, fontSize: \"14px\" },\n        formatter: function () {\n            const p = this.point.custom;\n            return `<b>${p.region}</b><br/>${p.product}: $${p.value}M (${p.share.toFixed(1)}% of region)`;\n        },\n    },\n    plotOptions: {\n        series: { animation: false },\n        column: { borderWidth: 0 },\n    },\n    series: [\n        ...products.map((name, j) => ({\n            type: \"column\",\n            name,\n            color: t.palette[j],\n            data: [],\n            showInLegend: true,\n        })),\n        {\n            type: \"scatter\",\n            name: \"Segments\",\n            showInLegend: false,\n            color: \"transparent\",\n            data: tooltipPoints,\n            marker: {\n                enabled: false,\n                states: { hover: { enabled: true, radius: 6, fillColor: t.ink, lineWidth: 0 } },\n            },\n        },\n    ],\n});\n"}