{"spec_id":"skewt-logp-atmospheric","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// skewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-26\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Thermodynamic helpers ---------------------------------------------------\nconst GAS_CONSTANT_DRY = 287.05; // J / (kg K)\nconst SPECIFIC_HEAT_DRY = 1005.7; // J / (kg K)\nconst LATENT_HEAT_VAPORIZATION = 2501000; // J / kg\nconst MIXING_RATIO_EPSILON = 0.622;\nconst SKEW = 45; // deg C shift per log10 decade of pressure — sets the isotherm slant\n\nfunction skewX(tempC, pressureHpa) {\n  return tempC + SKEW * Math.log10(1000 / pressureHpa);\n}\n\nfunction saturationVaporPressure(tempC) {\n  return 6.112 * Math.exp((17.67 * tempC) / (tempC + 243.5));\n}\n\nfunction saturationMixingRatio(tempC, pressureHpa) {\n  const vaporPressure = saturationVaporPressure(tempC);\n  return (MIXING_RATIO_EPSILON * vaporPressure) / (pressureHpa - vaporPressure);\n}\n\nfunction mixingRatioToTemp(mixingRatio, pressureHpa) {\n  const vaporPressure = (mixingRatio * pressureHpa) / (MIXING_RATIO_EPSILON + mixingRatio);\n  const logTerm = Math.log(vaporPressure / 6.112);\n  return (243.5 * logTerm) / (17.67 - logTerm);\n}\n\nfunction dryAdiabatTemp(potentialTempC, pressureHpa) {\n  const potentialTempK = potentialTempC + 273.15;\n  return potentialTempK * Math.pow(pressureHpa / 1000, GAS_CONSTANT_DRY / SPECIFIC_HEAT_DRY) - 273.15;\n}\n\nfunction moistLapseRate(tempK, pressureHpa) {\n  const rs = saturationMixingRatio(tempK - 273.15, pressureHpa);\n  const numerator = GAS_CONSTANT_DRY * tempK + LATENT_HEAT_VAPORIZATION * rs;\n  const denominator = SPECIFIC_HEAT_DRY\n    + (LATENT_HEAT_VAPORIZATION * LATENT_HEAT_VAPORIZATION * rs * MIXING_RATIO_EPSILON) / (GAS_CONSTANT_DRY * tempK * tempK);\n  return numerator / (pressureHpa * denominator);\n}\n\n// Numerically integrates the saturated-adiabatic lapse rate from 1000 hPa\n// through each sample pressure — moist adiabats have no closed-form solution.\nfunction moistAdiabatCurve(startTempC, pressures) {\n  let tempK = startTempC + 273.15;\n  let pressure = 1000;\n  return pressures.map((targetPressure) => {\n    const steps = Math.max(1, Math.round(Math.abs(pressure - targetPressure) / 5));\n    const step = (targetPressure - pressure) / steps;\n    for (let i = 0; i < steps; i += 1) {\n      tempK += moistLapseRate(tempK, pressure) * step;\n      pressure += step;\n    }\n    return [skewX(tempK - 273.15, targetPressure), targetPressure];\n  });\n}\n\n// --- Sounding data (synthetic radiosonde profile) ---------------------------\nconst pressureLevels = [1000, 975, 950, 925, 900, 850, 800, 750, 700, 650,\n  600, 550, 500, 450, 400, 350, 300, 250, 200, 150, 100];\n// Includes a shallow surface-based inversion (1000-950 hPa warms before\n// cooling resumes) and a near-saturated layer around 700 hPa (dewpoint\n// depression narrows to 0.5 degC) to showcase more of the diagram's range.\nconst temperatureC = [16, 17.5, 18.5, 17, 14.5, 10, 6, 2, -2.5, -7.5,\n  -13, -18.5, -24.5, -30.5, -37, -44, -50, -55, -58.5, -59.5, -61];\nconst dewpointC = [13, 13.5, 13, 12, 9, 5, 1, 0, -3, -9,\n  -16, -23, -30, -37, -44, -51, -58, -64, -68, -70, -72];\n\nconst temperaturePoints = pressureLevels.map((p, i) => [skewX(temperatureC[i], p), p]);\nconst dewpointPoints = pressureLevels.map((p, i) => [skewX(dewpointC[i], p), p]);\n\n// --- Reference-line families (isotherms, adiabats, mixing ratios) -----------\nconst referencePressures = [];\nfor (let p = 1000; p >= 100; p -= 50) referencePressures.push(p);\nconst mixingRatioPressures = referencePressures.filter((p) => p >= 400);\n\nconst isothermSeries = [];\nfor (let tempC = -80; tempC <= 40; tempC += 10) {\n  isothermSeries.push({\n    name: \"Isotherms\",\n    type: \"line\",\n    data: [[skewX(tempC, 1000), 1000], [skewX(tempC, 100), 100]],\n    color: t.inkSoft,\n    opacity: 0.3,\n    lineWidth: 1,\n    showInLegend: tempC === -80,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n  });\n}\n\nconst dryAdiabatSeries = [];\nfor (let potentialTempC = -20; potentialTempC <= 100; potentialTempC += 20) {\n  dryAdiabatSeries.push({\n    name: \"Dry adiabats\",\n    type: \"line\",\n    data: referencePressures.map((p) => [skewX(dryAdiabatTemp(potentialTempC, p), p), p]),\n    color: t.palette[3],\n    dashStyle: \"ShortDash\",\n    lineWidth: 1.25,\n    opacity: 0.6,\n    showInLegend: potentialTempC === -20,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n  });\n}\n\nconst moistAdiabatSeries = [];\nfor (let startTempC = -8; startTempC <= 32; startTempC += 8) {\n  moistAdiabatSeries.push({\n    name: \"Moist adiabats\",\n    type: \"line\",\n    data: moistAdiabatCurve(startTempC, referencePressures),\n    color: t.palette[2],\n    dashStyle: \"ShortDot\",\n    lineWidth: 1.25,\n    opacity: 0.6,\n    showInLegend: startTempC === -8,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n  });\n}\n\nconst mixingRatioSeries = [];\nfor (const mixingRatioGkg of [1, 2, 4, 7, 10, 16]) {\n  mixingRatioSeries.push({\n    name: \"Mixing ratio\",\n    type: \"line\",\n    data: mixingRatioPressures.map((p) => [skewX(mixingRatioToTemp(mixingRatioGkg / 1000, p), p), p]),\n    color: t.muted,\n    dashStyle: \"Dot\",\n    lineWidth: 1,\n    opacity: 0.4,\n    showInLegend: mixingRatioGkg === 1,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n  });\n}\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: { type: \"line\", backgroundColor: \"transparent\", animation: false,\n           style: { fontFamily: \"inherit\" } },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: { text: \"skewt-logp-atmospheric · javascript · highcharts · anyplot.ai\",\n           style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" } },\n  subtitle: { text: \"Synthetic radiosonde sounding — temperature skewed 45° against log-pressure\",\n              style: { color: t.inkSoft, fontSize: \"14px\" } },\n  xAxis: {\n    min: -45,\n    max: 45,\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    title: { text: \"Temperature (°C)\",\n             style: { color: t.inkSoft, fontSize: \"16px\" } },\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    type: \"logarithmic\",\n    reversed: true,\n    min: 100,\n    max: 1000,\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    title: { text: \"Pressure (hPa)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    labels: { formatter() { return this.value; }, style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    formatter() {\n      return `${this.series.name}<br>Pressure: ${this.y} hPa`;\n    },\n  },\n  plotOptions: { series: { animation: false } },\n  series: [\n    ...isothermSeries,\n    ...dryAdiabatSeries,\n    ...moistAdiabatSeries,\n    ...mixingRatioSeries,\n    {\n      name: \"Dewpoint\",\n      type: \"line\",\n      data: dewpointPoints,\n      color: t.palette[1],\n      dashStyle: \"Dash\",\n      lineWidth: 3,\n      // Zones color-segment the 650-750 hPa band amber: dewpoint depression\n      // narrows to ~0.5 degC there, i.e. a near-saturated (likely cloudy) layer.\n      zoneAxis: \"y\",\n      zones: [\n        { value: 650, color: t.palette[1], dashStyle: \"Dash\" },\n        { value: 750, color: t.amber, dashStyle: \"Dash\", lineWidth: 5 },\n      ],\n      marker: { enabled: true, symbol: \"circle\", radius: 4, fillColor: t.palette[1] },\n    },\n    {\n      name: \"Temperature\",\n      type: \"line\",\n      data: temperaturePoints,\n      color: t.palette[0],\n      lineWidth: 3,\n      // Zones dash the tropopause-and-above segment (<=200 hPa), where the\n      // lapse rate flattens toward isothermal — a genuine per-segment\n      // Highcharts feature rather than a second flat-colored line series.\n      zoneAxis: \"y\",\n      zones: [{ value: 200, color: t.palette[0], dashStyle: \"Dash\" }],\n      marker: { enabled: true, symbol: \"circle\", radius: 4, fillColor: t.palette[0] },\n    },\n  ],\n});\n"}