{"spec_id":"skewt-logp-atmospheric","library":"muix","language":"javascript","code":"// anyplot.ai\n// skewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n//# anyplot-orientation: square\n// anyplot.ai\n// skewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-08-26\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsClipPath } from \"@mui/x-charts/ChartsClipPath\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Thermodynamic constants -------------------------------------------------\nconst P_SURFACE = 1000; // hPa — skew reference level (surface, bottom of chart)\nconst P_TOP = 100; // hPa — top of chart (lower stratosphere)\nconst KAPPA = 0.286; // Rd / cp — dry adiabatic exponent\nconst R_D = 287; // J / (kg K) — gas constant for dry air\nconst C_P = 1004; // J / (kg K) — specific heat of dry air at constant pressure\nconst L_V = 2.5e6; // J / kg — latent heat of vaporization\nconst EPSILON = 0.622; // ratio of gas constants (Rd / Rv)\n\n// --- Pressure grid (deterministic, radiosonde-like resolution) --------------\nconst PRESSURE_LEVELS = [];\nfor (let p = P_SURFACE; p >= P_TOP; p -= 25) PRESSURE_LEVELS.push(p);\n\n// --- Atmospheric profile model (deterministic, no RNG needed) ---------------\nfunction heightFromPressure(pressureHpa) {\n  return 44330 * (1 - Math.pow(pressureHpa / P_SURFACE, 1 / 5.255));\n}\n\nfunction standardTemperatureC(heightM) {\n  const surfaceTempC = 22; // warm-season surface reading\n  const lapseRateCPerKm = 6.5;\n  const tropopauseHeightM = 11000;\n  const tropopauseTempC = surfaceTempC - lapseRateCPerKm * (tropopauseHeightM / 1000);\n  if (heightM <= tropopauseHeightM) {\n    return surfaceTempC - lapseRateCPerKm * (heightM / 1000);\n  }\n  return tropopauseTempC + 0.6 * ((heightM - tropopauseHeightM) / 1000);\n}\n\nfunction saturationVaporPressureHpa(tempC) {\n  return 6.112 * Math.exp((17.67 * tempC) / (tempC + 243.5));\n}\n\nfunction dewpointFromVaporPressureC(vaporPressureHpa) {\n  const logRatio = Math.log(vaporPressureHpa / 6.112);\n  return (243.5 * logRatio) / (17.67 - logRatio);\n}\n\nfunction relativeHumidityFraction(pressureHpa) {\n  const dryingWithHeight = 0.15 + 0.55 * Math.max(0, (pressureHpa - P_TOP) / (P_SURFACE - P_TOP));\n  const embeddedMoistLayer = 0.35 * Math.exp(-Math.pow((pressureHpa - 400) / 60, 2));\n  return Math.min(0.95, dryingWithHeight + embeddedMoistLayer);\n}\n\nconst soundingTempC = PRESSURE_LEVELS.map((p) => standardTemperatureC(heightFromPressure(p)));\nconst soundingDewpointC = PRESSURE_LEVELS.map((p, i) => {\n  const es = saturationVaporPressureHpa(soundingTempC[i]);\n  const e = relativeHumidityFraction(p) * es;\n  return dewpointFromVaporPressureC(e);\n});\n\n// --- Reference-line families --------------------------------------------------\nfunction dryAdiabatTempC(surfaceTempC, pressureHpa) {\n  const thetaK = surfaceTempC + 273.15;\n  return thetaK * Math.pow(pressureHpa / P_SURFACE, KAPPA) - 273.15;\n}\nconst DRY_ADIABAT_ANCHORS_C = [];\nfor (let tc = -30; tc <= 100; tc += 10) DRY_ADIABAT_ANCHORS_C.push(tc);\n\nfunction moistAdiabatCurve(surfaceTempC) {\n  const points = [{ p: P_SURFACE, t: surfaceTempC }];\n  let temp = surfaceTempC;\n  let pressure = P_SURFACE;\n  const stepHpa = 5;\n  while (pressure > P_TOP) {\n    const es = saturationVaporPressureHpa(temp);\n    const ws = (EPSILON * es) / (pressure - es);\n    const tempK = temp + 273.15;\n    const numerator = 1 + (L_V * ws) / (R_D * tempK);\n    const denominator = 1 + (L_V * L_V * ws * EPSILON) / (C_P * R_D * tempK * tempK);\n    const dTdP = ((tempK / pressure) * (R_D / C_P) * numerator) / denominator;\n    temp += dTdP * -stepHpa;\n    pressure -= stepHpa;\n    points.push({ p: pressure, t: temp });\n  }\n  return points;\n}\nconst MOIST_ADIABAT_ANCHORS_C = [0, 5, 10, 15, 20, 25, 30];\n\nfunction mixingRatioTempC(mixingRatioGPerKg, pressureHpa) {\n  const w = mixingRatioGPerKg / 1000;\n  const vaporPressureHpa = (w * pressureHpa) / (EPSILON + w);\n  return dewpointFromVaporPressureC(vaporPressureHpa);\n}\nconst MIXING_RATIOS_G_PER_KG = [0.4, 1, 2, 3, 5, 8, 12, 16, 20];\nconst MIXING_RATIO_MIN_PRESSURE = 400; // conventionally shown lower/mid troposphere only\n\n// --- Custom overlay: draws the skewed coordinate system as clipped SVG ------\n// MUI X community has no built-in skew transform, so the temperature axis is\n// pre-skewed per pressure level (skewOffset) using the chart's own pixel\n// scales — this keeps the 45-degree isotherms mathematically exact regardless\n// of the margins MUI X computes for the axis labels.\nfunction SkewTOverlay() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const drawingArea = useDrawingArea();\n\n  const pxPerDegC = xScale(1) - xScale(0);\n  const skewOffsetC = (pressureHpa) => (yScale(P_SURFACE) - yScale(pressureHpa)) / pxPerDegC;\n  const toPixel = (tempC, pressureHpa) => [\n    xScale(tempC + skewOffsetC(pressureHpa)),\n    yScale(pressureHpa),\n  ];\n  const pathFor = (levels, tempAt) =>\n    levels\n      .map((p, i) => {\n        const [x, y] = toPixel(tempAt(p, i), p);\n        return `${i === 0 ? \"M\" : \"L\"}${x},${y}`;\n      })\n      .join(\" \");\n\n  const isotherms = [];\n  for (let tc = -120; tc <= 120; tc += 10) {\n    const [x1, y1] = toPixel(tc, P_SURFACE);\n    const [x2, y2] = toPixel(tc, P_TOP);\n    isotherms.push({ tc, x1, y1, x2, y2 });\n  }\n\n  const dryAdiabatPaths = DRY_ADIABAT_ANCHORS_C.map((anchor) =>\n    pathFor(PRESSURE_LEVELS, (p) => dryAdiabatTempC(anchor, p)),\n  );\n  const moistAdiabatPaths = MOIST_ADIABAT_ANCHORS_C.map((anchor) => {\n    const curve = moistAdiabatCurve(anchor);\n    return curve\n      .map((pt, i) => {\n        const [x, y] = toPixel(pt.t, pt.p);\n        return `${i === 0 ? \"M\" : \"L\"}${x},${y}`;\n      })\n      .join(\" \");\n  });\n  const mixingRatioLevels = PRESSURE_LEVELS.filter((p) => p >= MIXING_RATIO_MIN_PRESSURE);\n  const mixingRatioPaths = MIXING_RATIOS_G_PER_KG.map((w) =>\n    pathFor(mixingRatioLevels, (p) => mixingRatioTempC(w, p)),\n  );\n\n  const tempPath = pathFor(PRESSURE_LEVELS, (_p, i) => soundingTempC[i]);\n  const dewpointPath = pathFor(PRESSURE_LEVELS, (_p, i) => soundingDewpointC[i]);\n  const markerLevels = PRESSURE_LEVELS.map((p, i) => ({ p, i })).filter(\n    (_, i) => i % 4 === 0 && i !== PRESSURE_LEVELS.length - 1,\n  );\n\n  const clipId = \"skewt-plot-clip\";\n  const legendX = drawingArea.left + 18;\n  const legendY = drawingArea.top + 18;\n  const legendRows = [\n    { label: \"Temperature\", color: t.palette[0], dash: \"0\" },\n    { label: \"Dewpoint\", color: t.palette[2], dash: \"10 6\" },\n    { label: \"Dry adiabat\", color: t.inkSoft, dash: \"6 4\" },\n    { label: \"Moist adiabat\", color: t.inkSoft, dash: \"1 4\" },\n    { label: \"Mixing ratio\", color: t.inkSoft, dash: \"3 2 1 2\" },\n  ];\n  const legendRowHeight = 26;\n  const legendHeight = legendRows.length * legendRowHeight + 16;\n  const legendWidth = 190;\n\n  return (\n    <>\n      <ChartsClipPath id={clipId} />\n      <g clipPath={`url(#${clipId})`}>\n        {isotherms.map((l) => (\n          <line\n            key={`iso-${l.tc}`}\n            x1={l.x1}\n            y1={l.y1}\n            x2={l.x2}\n            y2={l.y2}\n            stroke={l.tc === 0 ? t.amber : t.grid}\n            strokeWidth={l.tc === 0 ? 2.5 : 1.25}\n          />\n        ))}\n        {dryAdiabatPaths.map((d, i) => (\n          <path\n            key={`dry-${i}`}\n            d={d}\n            fill=\"none\"\n            stroke={t.inkSoft}\n            strokeWidth={1.25}\n            strokeDasharray=\"6 4\"\n            opacity={0.55}\n          />\n        ))}\n        {moistAdiabatPaths.map((d, i) => (\n          <path\n            key={`moist-${i}`}\n            d={d}\n            fill=\"none\"\n            stroke={t.inkSoft}\n            strokeWidth={1.25}\n            strokeDasharray=\"1 4\"\n            opacity={0.55}\n          />\n        ))}\n        {mixingRatioPaths.map((d, i) => (\n          <path\n            key={`mix-${i}`}\n            d={d}\n            fill=\"none\"\n            stroke={t.inkSoft}\n            strokeWidth={1.25}\n            strokeDasharray=\"3 2 1 2\"\n            opacity={0.45}\n          />\n        ))}\n        <path d={dewpointPath} fill=\"none\" stroke={t.palette[2]} strokeWidth={3} strokeDasharray=\"10 6\" />\n        <path d={tempPath} fill=\"none\" stroke={t.palette[0]} strokeWidth={3.5} />\n        {markerLevels.map(({ p, i }) => {\n          const [xT, yT] = toPixel(soundingTempC[i], p);\n          const [xD, yD] = toPixel(soundingDewpointC[i], p);\n          return (\n            <g key={`mark-${p}`}>\n              <circle cx={xT} cy={yT} r={5} fill={t.palette[0]} stroke={t.pageBg} strokeWidth={1.5} />\n              <circle cx={xD} cy={yD} r={5} fill={t.palette[2]} stroke={t.pageBg} strokeWidth={1.5} />\n            </g>\n          );\n        })}\n      </g>\n      <rect\n        x={legendX}\n        y={legendY}\n        width={legendWidth}\n        height={legendHeight}\n        rx={6}\n        fill={t.elevatedBg}\n        stroke={t.grid}\n        strokeWidth={1}\n      />\n      {legendRows.map((row, i) => {\n        const rowY = legendY + 16 + i * legendRowHeight + legendRowHeight / 2;\n        return (\n          <g key={row.label}>\n            <line\n              x1={legendX + 14}\n              y1={rowY}\n              x2={legendX + 44}\n              y2={rowY}\n              stroke={row.color}\n              strokeWidth={row.dash === \"0\" ? 3 : 2}\n              strokeDasharray={row.dash === \"0\" ? undefined : row.dash}\n            />\n            <text x={legendX + 54} y={rowY} dy=\"0.32em\" fontSize={15} fill={t.ink}>\n              {row.label}\n            </text>\n          </g>\n        );\n      })}\n    </>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const titleHeight = 52;\n  const chartHeight = window.ANYPLOT_SIZE.height - titleHeight;\n\n  return (\n    <Box\n      sx={{\n        width: window.ANYPLOT_SIZE.width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Typography sx={{ fontSize: 22, fontWeight: 600, pl: 1, pt: 0.5 }}>\n        skewt-logp-atmospheric · javascript · muix · anyplot.ai\n      </Typography>\n      <ChartContainer\n        width={window.ANYPLOT_SIZE.width}\n        height={chartHeight}\n        margin={{ top: 20, right: 32, bottom: 64, left: 88 }}\n        series={[]}\n        skipAnimation\n        xAxis={[\n          {\n            id: \"temperature\",\n            scaleType: \"linear\",\n            min: -40,\n            max: 45,\n            label: \"Temperature (°C, at surface)\",\n            tickNumber: 9,\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"pressure\",\n            scaleType: \"log\",\n            reverse: true,\n            min: P_TOP,\n            max: P_SURFACE,\n            label: \"Pressure (hPa)\",\n            tickInterval: [1000, 850, 700, 500, 400, 300, 250, 200, 150, 100],\n            valueFormatter: (v) => `${v}`,\n          },\n        ]}\n      >\n        <SkewTOverlay />\n        <ChartsXAxis axisId=\"temperature\" />\n        <ChartsYAxis axisId=\"pressure\" />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}