{"spec_id":"polar-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// polar-basic: Basic Polar Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 89/100 | Updated: 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. We project each (hour, visits)\n// polar coordinate to Cartesian ourselves. The grid rings, spokes, and the\n// translucent data area still need the core SVG renderer (there is no\n// native equivalent without highcharts-more), but the data markers, the\n// radial-scale numbers, and the hour labels are all real Highcharts series\n// bound to the projected coordinates — native marker/dataLabels APIs\n// instead of renderer text/circles — same polar-projection technique the\n// radar-basic implementation uses, with more of the chrome on real APIs.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (average website visits by hour of day, 24-hour cycle) -----------\nconst HOURS = Array.from({ length: 24 }, (_, i) => i);\nconst VISITS = [\n  850, 620, 480, 390, 350, 420, 680, 1200, 1900, 2400, 2650, 2800, 2900, 2950,\n  2850, 2700, 2600, 2750, 3100, 3600, 3950, 3700, 2900, 1800,\n];\nconst MAX_VALUE = 4000;\nconst RING_LEVELS = [1000, 2000, 3000, 4000];\nconst SPOKE_HOURS = [0, 3, 6, 9, 12, 15, 18, 21];\nconst SPOKE_LABELS = [\"12 AM\", \"3 AM\", \"6 AM\", \"9 AM\", \"12 PM\", \"3 PM\", \"6 PM\", \"9 PM\"];\nconst PEAK_HOUR = VISITS.indexOf(Math.max(...VISITS));\nconst TROUGH_HOUR = VISITS.indexOf(Math.min(...VISITS));\n\nconst formatHour = (hour) => {\n  const period = hour < 12 ? \"AM\" : \"PM\";\n  const h12 = hour % 12 === 0 ? 12 : hour % 12;\n  return `${h12}:00 ${period}`;\n};\n\nconst TITLE = \"polar-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: [150, 90, 90, 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: \"Average hourly visits over a 24-hour cycle\",\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.actualValue.toLocaleString()} visits`;\n    },\n  },\n  plotOptions: { series: { animation: false } },\n  series: [],\n});\n\n// Fix the (visible: false) axes to a known pixel-space extent so real series\n// can be data-bound at the same polar-projected coordinates the renderer\n// overlay uses below — this keeps the PNG output stable while giving the\n// interactive HTML genuine Highcharts series/tooltip/dataLabels usage.\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 - 70;\n\n// hour 0 points straight up (midnight); hours proceed clockwise like a clock face\nconst angleOf = (hour) => -Math.PI / 2 + (hour / 24) * (2 * Math.PI);\nconst pointAt = (hour, radiusFrac) => {\n  const angle = angleOf(hour);\n  return [cx + outerR * radiusFrac * Math.cos(angle), cy + outerR * radiusFrac * Math.sin(angle)];\n};\n// project a renderer-space [x, y] pixel into the fixed-extent axis coordinates\n// the data-bound series below use (yAxis increases upward, so flip y).\nconst toAxisXY = ([sx, sy]) => [sx - chart.plotLeft, chart.plotTop + chart.plotHeight - sy];\n\n// --- Radial grid rings (no native polar axis without highcharts-more) ----------\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 (geometry only; hour labels below are a real series) -------\nSPOKE_HOURS.forEach((hour) => {\n  const [ox, oy] = pointAt(hour, 1);\n  chart.renderer\n    .path([\"M\", cx, cy, \"L\", ox, oy])\n    .attr({ stroke: t.inkSoft, \"stroke-width\": 1, zIndex: 1 })\n    .add();\n});\n\n// --- Data curve (closed loop — hour 23 wraps back to hour 0) -------------------\nconst vertices = HOURS.map((hour) => pointAt(hour, VISITS[hour] / MAX_VALUE));\nconst path = [\"M\"];\nvertices.forEach(([x, y], i) => path.push(...(i === 0 ? [x, y] : [\"L\", x, y])));\npath.push(\"Z\");\n\nchart.renderer\n  .path(path)\n  .attr({\n    fill: t.palette[0],\n    \"fill-opacity\": 0.2,\n    stroke: t.palette[0],\n    \"stroke-width\": 3,\n    \"stroke-linejoin\": \"round\",\n    zIndex: 2,\n  })\n  .add();\n\n// --- Real Highcharts series ---------------------------------------------------\n// 1) Hourly visits: native scatter markers at the projected vertices (replaces\n//    a manual renderer-circle loop), with the peak/trough called out via the\n//    series' own dataLabels API instead of a static annotation.\nconst hourlyData = HOURS.map((hour, i) => {\n  const [ax, ay] = toAxisXY(vertices[i]);\n  const point = {\n    x: ax,\n    y: ay,\n    name: formatHour(hour),\n    custom: { actualValue: VISITS[hour] },\n  };\n  if (hour === PEAK_HOUR || hour === TROUGH_HOUR) {\n    const angle = angleOf(hour);\n    point.dataLabels = {\n      enabled: true,\n      format: `${hour === PEAK_HOUR ? \"Peak\" : \"Trough\"} · ${VISITS[hour].toLocaleString()}`,\n      align: Math.cos(angle) >= 0 ? \"left\" : \"right\",\n      verticalAlign: \"middle\",\n      x: Math.cos(angle) * 20,\n      y: Math.sin(angle) * 20,\n      style: { color: t.ink, fontSize: \"13px\", fontWeight: \"700\", textOutline: \"none\" },\n    };\n  }\n  return point;\n});\n\nchart.addSeries(\n  {\n    type: \"scatter\",\n    name: \"Hourly visits\",\n    zIndex: 1,\n    color: t.palette[0],\n    enableMouseTracking: true,\n    stickyTracking: false,\n    animation: false,\n    marker: {\n      enabled: true,\n      radius: 5,\n      fillColor: t.palette[0],\n      lineColor: t.pageBg,\n      lineWidth: 1.5,\n      states: { hover: { enabled: true, radius: 6, lineWidth: 1.5, lineColor: t.pageBg } },\n    },\n    dataLabels: { enabled: false, crop: false, overflow: \"allow\" },\n    data: hourlyData,\n  },\n  false\n);\n\n// 2) Radial scale: the ring numbers plus one explicit \"Visits\" unit label\n//    (VQ-06), rendered via dataLabels bound to real (invisible-marker) points\n//    instead of chart.renderer.text.\nconst ringLabelData = RING_LEVELS.map((level) => {\n  const [ax, ay] = toAxisXY(pointAt(0, level / MAX_VALUE));\n  return {\n    x: ax,\n    y: ay,\n    dataLabels: { format: level.toLocaleString(), align: \"left\", verticalAlign: \"middle\", x: 8, y: 4 },\n  };\n});\nconst [unitAx, unitAy] = toAxisXY(pointAt(0, RING_LEVELS[RING_LEVELS.length - 1] / MAX_VALUE));\nringLabelData.push({\n  x: unitAx,\n  y: unitAy,\n  dataLabels: {\n    format: \"Visits\",\n    align: \"left\",\n    verticalAlign: \"middle\",\n    x: 54,\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\n// 3) Hour of day: the 8 compass-style hour labels, again via dataLabels bound\n//    to real points instead of chart.renderer.text.\nconst hourLabelData = SPOKE_HOURS.map((hour, i) => {\n  const angle = angleOf(hour);\n  const cos = Math.cos(angle);\n  const sin = Math.sin(angle);\n  const [ax, ay] = toAxisXY(pointAt(hour, 1 + 30 / outerR));\n  const align = cos > 0.3 ? \"left\" : cos < -0.3 ? \"right\" : \"center\";\n  return {\n    x: ax,\n    y: ay,\n    dataLabels: { format: SPOKE_LABELS[i], align, verticalAlign: \"middle\", y: sin * 6 + 5 },\n  };\n});\n\nchart.addSeries(\n  {\n    type: \"scatter\",\n    name: \"Hour of day\",\n    zIndex: 3,\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: hourLabelData,\n  },\n  false\n);\n\nchart.redraw();\n"}