{"spec_id":"rose-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// rose-basic: Basic Rose Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-07-25\n//# anyplot-orientation: square\n\n// Only the core `highcharts` bundle is loaded (no highcharts-more), so the\n// native polar chart type isn't available. Each compass wedge is drawn with\n// the SVG renderer's own `arc()` primitive (the same primitive Highcharts\n// pie slices use internally), with radius scaled linearly to value so\n// segment length — not area — encodes the data, per the spec. A real\n// invisible-marker scatter point sits at each wedge tip so hover still\n// produces a genuine Highcharts tooltip instead of a static image.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (annual wind-observation hours by compass direction) -------------\nconst DIRECTIONS = [\"N\", \"NE\", \"E\", \"SE\", \"S\", \"SW\", \"W\", \"NW\"];\nconst FREQUENCY_HOURS = [620, 410, 380, 290, 340, 580, 710, 520];\nconst MAX_VALUE = 800;\nconst RING_LEVELS = [200, 400, 600, 800];\nconst SLICE_ANGLE = (2 * Math.PI) / DIRECTIONS.length;\nconst WEDGE_GAP = 0.025; // radians of empty space on each side of a wedge\n\nconst TITLE = \"rose-basic · javascript · highcharts · anyplot.ai\";\nconst titleFs = Math.round(22 * Math.min(1, 67 / TITLE.length)) + \"px\";\n\n// --- Chart (empty core chart used as a canvas for the renderer overlay) ----\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    margin: [130, 90, 70, 90],\n  },\n  credits: { enabled: false },\n  title: {\n    text: TITLE,\n    style: { color: t.ink, fontSize: titleFs, fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Wind frequency by compass direction\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: { visible: false, gridLineWidth: 0, lineWidth: 0, tickLength: 0 },\n  yAxis: { visible: false, gridLineWidth: 0, lineWidth: 0, tickLength: 0 },\n  legend: { enabled: false },\n  tooltip: {\n    enabled: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    style: { color: t.ink },\n    formatter() {\n      return `<b>${this.point.name}</b><br/>${this.point.custom.hours} hrs/yr`;\n    },\n  },\n  plotOptions: { series: { animation: false } },\n  series: [],\n});\n\n// Fix the (visible: false) axes to a known pixel-space extent so the tooltip\n// marker series below can be data-bound at the same projected coordinates\n// the renderer overlay uses — keeps the PNG stable while giving the\n// interactive HTML a genuine Highcharts series/tooltip.\nchart.xAxis[0].setExtremes(0, chart.plotWidth, false);\nchart.yAxis[0].setExtremes(0, chart.plotHeight, false);\n\n// --- Geometry ----------------------------------------------------------------\nconst cx = chart.plotLeft + chart.plotWidth / 2;\nconst cy = chart.plotTop + chart.plotHeight / 2;\nconst outerR = Math.min(chart.plotWidth, chart.plotHeight) / 2 - 60;\n\n// index 0 (N) points straight up; direction proceeds clockwise like a compass\nconst centerAngleOf = (i) => -Math.PI / 2 + i * SLICE_ANGLE;\nconst pointAt = (angle, radiusFrac) => [cx + outerR * radiusFrac * Math.cos(angle), cy + outerR * radiusFrac * Math.sin(angle)];\nconst toAxisXY = ([sx, sy]) => [sx - chart.plotLeft, chart.plotTop + chart.plotHeight - sy];\n\n// --- Radial grid rings -------------------------------------------------------\nRING_LEVELS.forEach((level) => {\n  chart.renderer\n    .circle(cx, cy, outerR * (level / MAX_VALUE))\n    .attr({ stroke: t.grid, \"stroke-width\": 1, fill: \"none\", zIndex: 1 })\n    .add();\n});\n\n// --- Angular spokes (one per compass direction) ------------------------------\nDIRECTIONS.forEach((_, i) => {\n  const [ox, oy] = pointAt(centerAngleOf(i), 1);\n  chart.renderer\n    .path([\"M\", cx, cy, \"L\", ox, oy])\n    .attr({ stroke: t.grid, \"stroke-width\": 1, zIndex: 1 })\n    .add();\n});\n\n// --- Wedges (radius proportional to value, not area) -------------------------\nDIRECTIONS.forEach((direction, i) => {\n  const center = centerAngleOf(i);\n  const radius = outerR * (FREQUENCY_HOURS[i] / MAX_VALUE);\n  chart.renderer\n    .arc(cx, cy, radius, 0, center - SLICE_ANGLE / 2 + WEDGE_GAP, center + SLICE_ANGLE / 2 - WEDGE_GAP)\n    .attr({ fill: t.palette[i], stroke: t.pageBg, \"stroke-width\": 2, zIndex: 2 })\n    .add();\n});\n\n// --- Real Highcharts series ---------------------------------------------------\n// 1) Wedge-tip markers: real data points bound at each wedge's outer radius,\n//    giving a genuine hover tooltip (category + value) over the static shape.\nconst wedgeData = DIRECTIONS.map((direction, i) => {\n  const angle = centerAngleOf(i);\n  const [ax, ay] = toAxisXY(pointAt(angle, FREQUENCY_HOURS[i] / MAX_VALUE));\n  return { x: ax, y: ay, name: direction, custom: { hours: FREQUENCY_HOURS[i] } };\n});\n\nchart.addSeries(\n  {\n    type: \"scatter\",\n    name: \"Wind frequency\",\n    zIndex: 3,\n    enableMouseTracking: true,\n    stickyTracking: false,\n    animation: false,\n    marker: {\n      enabled: true,\n      radius: 5,\n      lineWidth: 1.5,\n      lineColor: t.pageBg,\n    },\n    colors: t.palette,\n    colorByPoint: true,\n    dataLabels: { enabled: false },\n    data: wedgeData,\n  },\n  false\n);\n\n// 2) Compass labels: the 8 direction letters just outside the outermost ring.\nconst directionLabelData = DIRECTIONS.map((direction, i) => {\n  const [ax, ay] = toAxisXY(pointAt(centerAngleOf(i), 1 + 34 / outerR));\n  return { x: ax, y: ay, dataLabels: { format: direction, align: \"center\", verticalAlign: \"middle\" } };\n});\n\nchart.addSeries(\n  {\n    type: \"scatter\",\n    name: \"Compass direction\",\n    zIndex: 4,\n    enableMouseTracking: false,\n    animation: false,\n    marker: { enabled: false },\n    dataLabels: {\n      enabled: true,\n      crop: false,\n      overflow: \"allow\",\n      style: { color: t.ink, fontSize: \"15px\", fontWeight: \"600\", textOutline: \"none\" },\n    },\n    data: directionLabelData,\n  },\n  false\n);\n\n// 3) Radial scale: the ring numbers plus one explicit unit label (VQ-06),\n//    placed along a spoke-free bearing so they don't collide with a letter.\nconst scaleAngle = -Math.PI / 2 - SLICE_ANGLE / 2;\nconst ringLabelData = RING_LEVELS.map((level) => {\n  const [ax, ay] = toAxisXY(pointAt(scaleAngle, level / MAX_VALUE));\n  return { x: ax, y: ay, dataLabels: { format: String(level), align: \"left\", verticalAlign: \"middle\", x: 6, y: -4 } };\n});\nconst [unitAx, unitAy] = toAxisXY(pointAt(scaleAngle, RING_LEVELS[RING_LEVELS.length - 1] / MAX_VALUE));\nringLabelData.push({\n  x: unitAx,\n  y: unitAy,\n  dataLabels: {\n    format: \"hrs/yr\",\n    align: \"left\",\n    verticalAlign: \"middle\",\n    x: 46,\n    y: -4,\n    style: { color: t.inkSoft, fontSize: \"11px\", fontStyle: \"italic\", fontWeight: \"400\", textOutline: \"none\" },\n  },\n});\n\nchart.addSeries(\n  {\n    type: \"scatter\",\n    name: \"Radial scale\",\n    zIndex: 2,\n    enableMouseTracking: false,\n    animation: false,\n    marker: { enabled: false },\n    dataLabels: {\n      enabled: true,\n      crop: false,\n      overflow: \"allow\",\n      style: { color: t.inkSoft, fontSize: \"12px\", fontWeight: \"400\", textOutline: \"none\" },\n    },\n    data: ringLabelData,\n  },\n  false\n);\n\nchart.redraw();\n"}