{"spec_id":"slope-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// slope-basic: Basic Slope Chart (Slopegraph)\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 96/100 | Created: 2026-07-25\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Customer satisfaction score (0-100) for 10 products, two survey waves.\nconst products = [\n    { name: \"CloudSync\",   start: 62, end: 78 },\n    { name: \"DataVault\",   start: 88, end: 91 },\n    { name: \"PixelPro\",    start: 74, end: 65 },\n    { name: \"NetGuard\",    start: 55, end: 72 },\n    { name: \"FlowStream\",  start: 80, end: 76 },\n    { name: \"CoreLogic\",   start: 69, end: 84 },\n    { name: \"ByteWave\",    start: 91, end: 88 },\n    { name: \"GridPoint\",   start: 58, end: 61 },\n    { name: \"LinkHub\",     start: 77, end: 59 },\n    { name: \"TaskFlow\",    start: 66, end: 82 },\n];\n\nconst INCREASE = t.palette[0]; // brand green — profit/up/gain\nconst DECREASE = t.palette[4]; // matte red — loss/down\nconst FLAT = t.muted;\n\nconst lineOf = (p) => (p.end > p.start ? INCREASE : p.end < p.start ? DECREASE : FLAT);\n\n// Tighten the y-domain to the actual data band (55-91) instead of a fixed\n// 40-100 range, so the canvas isn't dominated by dead space.\nconst scores = products.flatMap((p) => [p.start, p.end]);\nconst yPad = 5;\nconst yMin = Math.floor(Math.min(...scores) - yPad);\nconst yMax = Math.ceil(Math.max(...scores) + yPad);\n\n// The mover with the largest absolute change gets a slightly heavier line\n// and marker so the standout swing reads at a glance.\nconst standout = products.reduce((a, p) => (Math.abs(p.end - p.start) > Math.abs(a.end - a.start) ? p : a));\n\n// Endpoint labels within NUDGE_GAP score points of a neighbor at the same\n// wave get nudged apart vertically (label only, marker stays put) so\n// stacked text doesn't crowd together.\nconst NUDGE_GAP = 3;\nconst NUDGE_PX = 6;\nconst labelNudges = (values) => {\n    const byValue = [...values].sort((a, b) => a.value - b.value);\n    const nudge = new Map(values.map((v) => [v.name, 0]));\n    for (let i = 1; i < byValue.length; i++) {\n        const lo = byValue[i - 1];\n        const hi = byValue[i];\n        if (hi.value - lo.value <= NUDGE_GAP) {\n            nudge.set(lo.name, nudge.get(lo.name) + NUDGE_PX);\n            nudge.set(hi.name, nudge.get(hi.name) - NUDGE_PX);\n        }\n    }\n    return nudge;\n};\nconst startNudge = labelNudges(products.map((p) => ({ name: p.name, value: p.start })));\nconst endNudge = labelNudges(products.map((p) => ({ name: p.name, value: p.end })));\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n    chart: {\n        type: \"line\",\n        backgroundColor: \"transparent\",\n        animation: false,\n        style: { fontFamily: \"inherit\" },\n        marginLeft: 170,\n        marginRight: 170,\n    },\n    credits: { enabled: false },\n    title: {\n        text: \"slope-basic · javascript · highcharts · anyplot.ai\",\n        style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n    },\n    xAxis: {\n        categories: [\"Survey Wave 1\", \"Survey Wave 2\"],\n        lineColor: t.inkSoft,\n        tickColor: t.inkSoft,\n        gridLineWidth: 0,\n        minPadding: 0.08,\n        maxPadding: 0.08,\n        labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    },\n    yAxis: {\n        title: {\n            text: \"Customer Satisfaction Score\",\n            style: { color: t.inkSoft, fontSize: \"16px\" },\n        },\n        min: yMin,\n        max: yMax,\n        startOnTick: false,\n        endOnTick: false,\n        gridLineWidth: 0,\n        lineColor: t.inkSoft,\n        tickColor: t.inkSoft,\n        labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    },\n    legend: {\n        enabled: true,\n        align: \"center\",\n        verticalAlign: \"bottom\",\n        itemStyle: { color: t.inkSoft, fontSize: \"14px\", fontWeight: \"normal\" },\n        itemHoverStyle: { color: t.ink },\n    },\n    tooltip: {\n        headerFormat: \"\",\n        pointFormat: \"<b>{series.name}</b>: {point.y}\",\n        backgroundColor: t.elevatedBg,\n        borderColor: t.inkSoft,\n        style: { color: t.ink, fontSize: \"13px\" },\n    },\n    plotOptions: {\n        series: {\n            animation: false,\n            marker: { enabled: true, radius: 5, symbol: \"circle\" },\n            lineWidth: 2,\n            dataLabels: {\n                enabled: true,\n                crop: false,\n                overflow: \"allow\",\n                style: { fontSize: \"14px\", fontWeight: \"normal\", textOutline: \"none\" },\n            },\n        },\n    },\n    series: [\n        // Legend-only entries — real product lines below are excluded from the legend\n        // since 10 individual names would overwhelm it; color already carries the story.\n        { name: \"Improved\", type: \"line\", color: INCREASE, data: [], marker: { enabled: true }, enableMouseTracking: false },\n        { name: \"Declined\", type: \"line\", color: DECREASE, data: [], marker: { enabled: true }, enableMouseTracking: false },\n        ...products.map((p) => {\n            const isStandout = p.name === standout.name;\n            return {\n                name: p.name,\n                showInLegend: false,\n                color: lineOf(p),\n                lineWidth: isStandout ? 3.5 : 2,\n                marker: { radius: isStandout ? 6.5 : 5 },\n                data: [\n                    {\n                        y: p.start,\n                        dataLabels: {\n                            align: \"right\",\n                            x: -12,\n                            y: startNudge.get(p.name),\n                            color: t.inkSoft,\n                            format: `${p.name} · ${p.start}`,\n                        },\n                    },\n                    {\n                        y: p.end,\n                        dataLabels: {\n                            align: \"left\",\n                            x: 12,\n                            y: endNudge.get(p.name),\n                            color: t.inkSoft,\n                            format: `${p.end} · ${p.name}`,\n                        },\n                    },\n                ],\n            };\n        }),\n    ],\n});\n"}