├── .gitignore ├── README.md ├── build.js ├── dist ├── bundle.js └── skewt.css ├── make_w_mod.js ├── package.json ├── src ├── atmosphere.mjs ├── clouds.mjs ├── d3.custom.min.mjs ├── math.mjs ├── skewt.less └── skewt.mjs └── windy_module ├── README.md ├── skewt.js ├── skewt.min.js └── skewtRegister.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .npmignore 4 | .gitignore 5 | atmosphere__.js 6 | move.js 7 | move_2_plugin.js 8 | move_2_windy_mod.js 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skewt-js 2 | Plot a skew-T log-P diagram based on sounding data. 3 | 4 | This was forked from: [https://github.com/dfelix/skewt-js](https://github.com/dfelix/skewt-js). 5 | 6 | ## Changes 7 | 8 | - By now LOTS. 9 | - Updated to work with D3 version 5. 10 | - A treeshaken D3 is now bundled into bundle.js, no need to load it. 11 | - Interactivity: Set top pressure (zoom), set gradient, set parcel temperature, toggle maintain temperature range. 12 | - Highlight different lines. 13 | 14 | I have added the following functionality thanks to [Victor Berchet](https://github.com/vicb/windy-plugin-sounding) : 15 | 16 | - Added moist adiabats. 17 | - Added isohumes. 18 | - Added parcel trajectory. 19 | 20 | 21 | Added calculations for TCON, LCL, CCL, Thermal top and cloud Top. Lines to indicate these parameters can be toggled. 22 | 23 | ## How to use 24 | 25 | ```html 26 | 27 | 28 | 29 | ``` 30 | 31 | Ensure you create a div using a specified id (ex: id="mySkewt") and class = "skew-t" 32 | 33 | ```html 34 |
35 | ``` 36 | 37 | Declare a new SkewT var passing the css selector for the placeholder. 38 | 39 | ```javascript 40 | var skewt = new SkewT('#mySkewt' , options); 41 | ``` 42 | 43 | SkewT currently only contains methods: 44 | 45 | ### Plot 46 | 47 | `.plot(array, plotOptions)` will plot dew point and air temperature lines and wind barbs. options is optional. 48 | 49 | Available options: 50 | 51 | ```javascript 52 | plotOptions = { 53 | add:true, // to add a plotline, else the current ones will be cleared. 54 | select:true, // added plot line will be highlighted, relevant if >1. 55 | max: 2// maximum number of plots superimposed, if max reached, last one will be overwritten, 56 | ixShift: 1// moves the windbarbs and clouds to the right, ( I use it in windy to differentiate between sonde data and forecast data ) 57 | } 58 | ``` 59 | 60 | ```javascript 61 | var skewt = new SkewT('#mySkewt'); 62 | var sounding = []; 63 | skewt.plot(sounding, plotOptions); 64 | ``` 65 | 66 | Expected array format should follow the GSD Sounding Format. 67 | 68 | ```javascript 69 | [{ 70 | "press": 1000.0, // pressure in whole millibars 71 | "hght": 173.0, // height in meters (m) 72 | "temp": 14.1, // temperature in degrees Celsius 73 | "dwpt": 6.5, // dew point temperature in degree Celsius 74 | "wdir": 8.0, // wind direction in degrees 75 | "wspd": 6.173 // wind speed in meters per second (m/s) 76 | }, { 77 | "press": 975.0, 78 | "hght": 386.0, 79 | "temp": 12.1, 80 | "dwpt": 5.6, 81 | "wdir": 10.0, 82 | "wspd": 7.716 83 | }, 84 | ... 85 | ] 86 | ``` 87 | 88 | ### Clear 89 | 90 | `.clear()` will clear the previous plot lines and wind barbs. 91 | 92 | ### Select a skewt plot 93 | 94 | `.selectSkewt( array_previously_sent_with_plot )` to highlight a plot lines. The tooltips will then display info from this line. 95 | 96 | ### Remove a specific plot, leave the others: 97 | 98 | `.removePlot` //remove a specific plot, referenced by data object passed initially. 99 | 100 | 101 | 102 | ### Parcel trajectory calculations can be done, without actually plotting it: 103 | 104 | ```javascript 105 | .parcelTrajectory( 106 | {temp, gh, level}, //arrays, must be same length 107 | steps, // integer 108 | surface_temp, surface_pressure, surface_dewpoint 109 | ) 110 | ``` 111 | ### set or get Params 112 | ```javascript 113 | .setParams( 114 | { 115 | height, //in pixels 116 | topp, //top pressure 117 | parctempShift, //default parcel temperature offset 118 | parctemp, 119 | basep, 120 | steph, //step height 121 | gradient //in degrees 122 | } 123 | ) 124 | .getParams(); 125 | ``` 126 | ### Tooltips: 127 | 128 | `.move2P`, `.hideTooltips` and `.showTooltips` 129 | 130 | ### Listener 131 | 132 | `.on(event, callback)`; 133 | 134 | Possible events: 135 | 136 | - `press`, //when tooltip is moved. 137 | - `parctemp, topp, parctempShift, gradient`; //when any of the ranges are moved 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | rollup = require('rollup'), 4 | less = require('less'), 5 | babel = require("@babel/core"), 6 | minify = require("babel-minify"); 7 | translate2windyMod = require("./make_w_mod"); 8 | 9 | const inputOptions = {input: "src/skewt.mjs"}; 10 | const outputOptions = {file:"dist/bundle.js", format:"iife", compact:true, minifyInternalExports:true}; 11 | 12 | const files=["d3.custom.min.mjs","clouds.mjs","math.mjs","skewt.mjs","atmosphere.mjs","skewt.less"]; 13 | 14 | async function build() { 15 | let bundle; 16 | try { 17 | bundle = await rollup.rollup(inputOptions); 18 | } catch(er){ 19 | console.log("Error",er); 20 | return 21 | } 22 | let result = await bundle.generate(outputOptions); 23 | await bundle.close(); 24 | return result.output[0].code; 25 | } 26 | 27 | async function minifyCode(c){ 28 | c = babel.transformSync( c ,{ 29 | presets: ["@babel/preset-env"] 30 | }); 31 | return minify(c.code).code; 32 | } 33 | 34 | let building = false; 35 | async function main(f){ 36 | if (building) { 37 | console.log("busy try again"); 38 | return; 39 | } 40 | building=true; 41 | console.log(f); 42 | const lessSrc = await fs.readFileSync("src/skewt.less", 'utf8'); 43 | let { css } = await less.render(lessSrc, { 44 | cleancss: true, 45 | compress: true, 46 | }).catch(console.log); 47 | fs.writeFileSync("dist/skewt.css", css); 48 | console.log("start build") 49 | let code = await build();//with rollup 50 | 51 | //code = await minifyCode(code); 52 | if (code){ 53 | fs.writeFileSync("dist/bundle.js", code); 54 | 55 | //translate to windy modules 56 | let code4windyMod= translate2windyMod(code); 57 | let min4windy= await minifyCode(code4windyMod); 58 | 59 | fs.writeFileSync("windy_module/skewt.js",code4windyMod,'utf8'); 60 | fs.writeFileSync("windy_module/skewt.min.js",min4windy,'utf8'); 61 | 62 | console.log("done"); 63 | } else { 64 | console.log("error, fix it and save again") 65 | } 66 | building=false; 67 | } 68 | 69 | main("start watching"); 70 | 71 | files.forEach(f=> 72 | fs.watchFile("src/"+f,()=>main(f+" changed")) 73 | ); 74 | -------------------------------------------------------------------------------- /dist/skewt.css: -------------------------------------------------------------------------------- 1 | .skew-t{position:relative;padding:0px}.skew-t .fnt{transition:font .3s;font:10px Arial;font-family:-apple-system,system-ui,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif}.skew-t .mainsvg{background-color:transparent}.skew-t .controls,.skew-t .range-container{margin-top:10px}.skew-t .controls .buttons,.skew-t .range-container .buttons{flex-grow:1;margin:3px;padding:3px 0 3.2px 0;border-radius:10px;text-align:center;cursor:pointer;line-height:1.1em;background-color:#dcdcdc}.skew-t .controls .buttons.clicked,.skew-t .range-container .buttons.clicked{background-color:#969696;color:white}.skew-t .controls .buttons.units,.skew-t .range-container .buttons.units{flex-grow:0;width:80px}.skew-t .controls .buttons.noclick,.skew-t .range-container .buttons.noclick{cursor:initial}.skew-t .controls .row,.skew-t .range-container .row{display:flex;flex-wrap:wrap}.skew-t .controls{box-sizing:border-box;width:100%;display:flex}.skew-t .controls .values{flex-grow:1;margin:3px;padding:3px;text-align:center;border-radius:10px;border:1px solid #dcdcdc;min-width:40px}.skew-t .skewt-range-des{width:20%}.skew-t .skewt-range-val{width:15%;white-space:nowrap}.skew-t .checkbox-container{width:100%;line-height:20px}.skew-t .select-units :first-child{line-height:20px}.skew-t .axis path,.skew-t .axis line{fill:none;stroke:#000;stroke-width:1px;shape-rendering:crispEdges}.skew-t .axis{fill:#000}.skew-t .y.axis{font-size:10px}.skew-t .y.axis.hght{font-size:10px;fill:red}.skew-t .x.axis{font-size:10px}.skew-t .y.axis.ticks text{display:none}.skew-t .y.axis.hght-ticks text{display:none}.skew-t .y.axis.hght-ticks line{stroke:red}.skew-t .skewt-line{fill:none;stroke-width:1.5px;stroke-opacity:.5}.skew-t .skewt-line.highlight-line{stroke-opacity:1;stroke-width:2px}.skew-t .temp{fill:none;stroke-width:1.5px;stroke-opacity:.5;stroke:red}.skew-t .temp.highlight-line{stroke-opacity:1;stroke-width:2px}.skew-t .dwpt{fill:none;stroke-width:1.5px;stroke-opacity:.5;stroke:blue}.skew-t .dwpt.highlight-line{stroke-opacity:1;stroke-width:2px}.skew-t .parcel{fill:none;stroke-width:1.5px;stroke-opacity:.5;stroke:green;stroke-opacity:.3}.skew-t .parcel.highlight-line{stroke-opacity:1;stroke-width:2px}.skew-t .cond-level{fill:none;stroke-width:1.5px;stroke-opacity:.5;stroke-width:1px;stroke:rgba(128,128,128,0.8);stroke-opacity:.15}.skew-t .cond-level.highlight-line{stroke-opacity:1;stroke-width:2px}.skew-t .cond-level.highlight-line{stroke-width:1px}.skew-t .gridline{stroke-width:.5px;stroke-opacity:.3;fill:none}.skew-t .gridline.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .tempzero{stroke-width:.5px;stroke-opacity:.3;fill:none;stroke:#aaa;stroke-width:1.25px}.skew-t .tempzero.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .dryadiabat{stroke-width:.5px;stroke-opacity:.3;fill:none;stroke:green}.skew-t .dryadiabat.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .templine{stroke-width:.5px;stroke-opacity:.3;fill:none;stroke:red}.skew-t .templine.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .pressure{stroke-width:.5px;stroke-opacity:.3;fill:none;stroke:#787878}.skew-t .pressure.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .moistadiabat{stroke-width:.5px;stroke-opacity:.3;fill:none;stroke:green;stroke-dasharray:5}.skew-t .moistadiabat.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .isohume{stroke-width:.5px;stroke-opacity:.3;fill:none;stroke:blue;stroke-dasharray:2}.skew-t .isohume.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .elr{stroke-width:.5px;stroke-opacity:.3;fill:none;stroke:purple;stroke-opacity:.03;stroke-width:3px}.skew-t .elr.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .elr.highlight-line{stroke-opacity:.7}.skew-t .sigline{stroke-width:.5px;stroke-opacity:.3;fill:none}.skew-t .sigline.highlight-line{stroke-opacity:1;stroke-width:1px}.skew-t .sigline.surface{stroke:green}.skew-t .sigline.tropopause-level{stroke:blue}.skew-t .windbarb{stroke:#000;stroke-width:.75px;fill:none}.skew-t .windbarb .barblines{opacity:.4}.skew-t .windbarb .barblines.highlight-line{opacity:1}.skew-t .windbarb .barblines.hidden{display:none}.skew-t .windbarb .windtext{opacity:.4;dominant-baseline:central;font-size:10px;fill:black;stroke-width:0}.skew-t .windbarb .windtext.highlight-line{opacity:1}.skew-t .windbarb .windtext.hidden{display:none}.skew-t .flag{fill:#000}.skew-t .overlay{fill:none;pointer-events:all}.skew-t .focus.tmpc circle{fill:red;stroke:none}.skew-t .focus.dwpc circle{fill:blue;stroke:none}.skew-t .focus text{font-size:14px}.skew-t .skewt-wind-arrow{alignment-baseline:middle;text-anchor:middle;fill:black;font-size:16px;font-weight:bold}.skew-t .range-container-extra{margin-top:10px;margin-top:0px}.skew-t .range-container-extra .buttons{flex-grow:1;margin:3px;padding:3px 0 3.2px 0;border-radius:10px;text-align:center;cursor:pointer;line-height:1.1em;background-color:#dcdcdc}.skew-t .range-container-extra .buttons.clicked{background-color:#969696;color:white}.skew-t .range-container-extra .buttons.units{flex-grow:0;width:80px}.skew-t .range-container-extra .buttons.noclick{cursor:initial}.skew-t .range-container-extra .row{display:flex;flex-wrap:wrap}.skew-t .skewt-ranges{all:revert;-webkit-appearance:none;color:white;background-color:transparent;width:60%}.skew-t .skewt-ranges:focus{outline:none}.skew-t .skewt-ranges::-webkit-slider-runnable-track{all:revert;-webkit-appearance:none;background:#dcdcdc;border-radius:16px;height:16px;padding:2px}.skew-t .skewt-ranges::-webkit-slider-thumb{all:revert;-webkit-appearance:none;border:1px solid #646464;height:12px;width:12px;border-radius:12px;background:#ffffff;cursor:pointer;box-shadow:1px 1px 1px #000000}.skew-t .flex-break{flex-basis:100%}.skew-t .cloud-container{overflow:hidden;position:absolute;width:20px;opacity:.8}.skew-t .cloud-container .cloud{position:absolute;width:10px} -------------------------------------------------------------------------------- /make_w_mod.js: -------------------------------------------------------------------------------- 1 | // this is called anyway by build.js 2 | 3 | const fs = require("fs"); 4 | 5 | let file=fs.readFileSync("dist/bundle.js",'utf8'); 6 | let css=fs.readFileSync("dist/skewt.css",'utf8'); 7 | 8 | function translate2windyMod(file){ 9 | 10 | file= 11 | `if(!W['@plugins/skewt']) W.define( 12 | '@plugins/skewt', 13 | [], 14 | function (__exports) { 15 | 'use strict'; 16 | console.log("skewt loaded"); 17 | // `+file; 18 | 19 | file = file.replace('window.SkewT', 'this.SkewT' ); 20 | 21 | let lastpos=file.lastIndexOf("());") 22 | file= file.slice(0,lastpos)+ 23 | ` 24 | , 25 | false 26 | , 27 | \`${css}\` 28 | )`; 29 | return file; 30 | } 31 | 32 | module.exports = translate2windyMod; 33 | 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skewt-plus", 3 | "version": "1.1.0", 4 | "description": "skewt-plus", 5 | "main": "dist/bundle.js", 6 | "scripts": { 7 | "start": "node build.js", 8 | "rollup": "rollup src/skewt.mjs --format iife --name \"myBundle\" --file dist/bundle.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/rittels/skewt-js.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/rittels/skewt-js/issues" 18 | }, 19 | "homepage": "https://github.com/rittels/skewt-js#readme", 20 | "devDependencies": { 21 | "@babel/preset-env": "^7.12.17", 22 | "babel-minify": "^0.5.1", 23 | "less": "^4.1.1", 24 | "rollup": "^2.39.0" 25 | }, 26 | "dependencies": { 27 | "@babel/core": "^7.12.17" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/atmosphere.mjs: -------------------------------------------------------------------------------- 1 | import math from "./math.mjs"; 2 | 3 | // Gas constant for dry air at the surface of the Earth 4 | const Rd = 287; 5 | // Specific heat at constant pressure for dry air 6 | const Cpd = 1005; 7 | // Molecular weight ratio 8 | const epsilon = 18.01528 / 28.9644; 9 | // Heat of vaporization of water 10 | const Lv = 2501000; 11 | // Ratio of the specific gas constant of dry air to the specific gas constant for water vapour 12 | const satPressure0c = 6.112; 13 | // C + celsiusToK -> K 14 | const celsiusToK = 273.15; 15 | const L = -6.5e-3; 16 | const g = 9.80665; 17 | 18 | /** 19 | * Computes the temperature at the given pressure assuming dry processes. 20 | * 21 | * t0 is the starting temperature at p0 (degree Celsius). 22 | */ 23 | 24 | 25 | 26 | //export 27 | function dryLapse(p, tK0, p0) { 28 | return tK0 * Math.pow(p / p0, Rd / Cpd); 29 | } 30 | 31 | 32 | //to calculate isohume lines: 33 | //1. Obtain saturation vapor pressure at a specific temperature = partial pressure at a specific temp where the air will be saturated. 34 | //2. Mixing ratio: Use the partial pressure where air will be saturated and the actual pressure to determine the degree of mixing, thus what % of air is water. 35 | //3. Having the mixing ratio at the surface, calculate the vapor pressure at different pressures. 36 | //4. Dewpoint temp can then be calculated with the vapor pressure. 37 | 38 | // Computes the mixing ration of a gas. 39 | //export 40 | function mixingRatio(partialPressure, totalPressure, molecularWeightRatio = epsilon) { 41 | return (molecularWeightRatio * partialPressure) / (totalPressure - partialPressure); 42 | } 43 | 44 | // Computes the saturation mixing ratio of water vapor. 45 | //export 46 | function saturationMixingRatio(p, tK) { 47 | return mixingRatio(saturationVaporPressure(tK), p); 48 | } 49 | 50 | // Computes the saturation water vapor (partial) pressure 51 | //export 52 | function saturationVaporPressure(tK) { 53 | const tC = tK - celsiusToK; 54 | return satPressure0c * Math.exp((17.67 * tC) / (tC + 243.5)); 55 | } 56 | 57 | // Computes the temperature gradient assuming liquid saturation process. 58 | //export 59 | function moistGradientT(p, tK) { 60 | const rs = saturationMixingRatio(p, tK); 61 | const n = Rd * tK + Lv * rs; 62 | const d = Cpd + (Math.pow(Lv, 2) * rs * epsilon) / (Rd * Math.pow(tK, 2)); 63 | return (1 / p) * (n / d); 64 | } 65 | 66 | // Computes water vapor (partial) pressure. 67 | //export 68 | function vaporPressure(p, mixing) { 69 | return (p * mixing) / (epsilon + mixing); 70 | } 71 | 72 | // Computes the ambient dewpoint given the vapor (partial) pressure. 73 | //export 74 | function dewpoint(p) { 75 | const val = Math.log(p / satPressure0c); 76 | return celsiusToK + (243.5 * val) / (17.67 - val); 77 | } 78 | 79 | //export 80 | function getElevation(p, p0 = 1013.25) { 81 | const t0 = 288.15; 82 | //const p0 = 1013.25; 83 | return (t0 / L) * (Math.pow(p / p0, (-L * Rd) / g) - 1); 84 | } 85 | 86 | //export 87 | function getElevation2(p, refp = 1013.25) { //pressure altitude with NOAA formula (https://en.wikipedia.org/wiki/Pressure_altitude) 88 | return 145366.45 * (1 - Math.pow(p / refp, 0.190284)) / 3.28084; 89 | } 90 | 91 | //export 92 | function pressureFromElevation(e, refp = 1013.25) { 93 | e = e * 3.28084; 94 | return Math.pow((-(e / 145366.45 - 1)), 1 / 0.190284) * refp; 95 | } 96 | 97 | //export 98 | function getSurfaceP(surfElev, refElev = 110.8, refP = 1000) { //calculate surface pressure at surfelev, from reference elev and ref pressure. 99 | let expectElev = getElevation2(refP); 100 | let elevD = surfElev - refElev; 101 | return pressureFromElevation(elevD, refP); 102 | } 103 | 104 | //export 105 | 106 | /** 107 | * @param params = {temp, gh, level} 108 | * @param surface temp, pressure and dewpoint 109 | */ 110 | 111 | 112 | function parcelTrajectory(params, steps, sfcT, sfcP, sfcDewpoint) { 113 | 114 | //remove invalid or NaN values in params 115 | for (let i = 0; i < params.temp.length; i++) { 116 | let inval = false; 117 | for (let p in params) if (!params[p][i] && params[p][i] !== 0) inval = true; 118 | if (inval) for (let p in params) params[p].splice(i, 1); 119 | } 120 | 121 | const parcel = {}; 122 | const dryGhs = []; 123 | const dryPressures = []; 124 | const dryTemps = []; //dry temps from surface temp, which can be greater than templine start 125 | const dryDewpoints = []; 126 | const dryTempsTempline = []; //templine start 127 | 128 | const mRatio = mixingRatio(saturationVaporPressure(sfcDewpoint), sfcP); 129 | 130 | const pToEl = math.scaleLog(params.level, params.gh); 131 | const minEl = pToEl(sfcP); 132 | const maxEl = Math.max(minEl, params.gh[params.gh.length - 1]); 133 | const stepEl = (maxEl - minEl) / steps; 134 | 135 | const moistLineFromEandT = (elevation, t) => { 136 | //this calculates a moist line from elev and temp to the intersection of the temp line if the intersection exists otherwise very high cloudtop 137 | const moistGhs = [], moistPressures = [], moistTemps = []; 138 | let previousP = pToEl.invert(elevation); 139 | for (; elevation < maxEl + stepEl; elevation += stepEl) { 140 | const p = pToEl.invert(elevation); 141 | t = t + (p - previousP) * moistGradientT(p, t); 142 | previousP = p; 143 | moistGhs.push(elevation); 144 | moistPressures.push(p); 145 | moistTemps.push(t); 146 | } 147 | let moist = math.zip(moistTemps, moistPressures); 148 | let cloudTop, pCloudTop; 149 | const equilibrium = math.firstIntersection(moistGhs, moistTemps, params.gh, params.temp); 150 | 151 | if (moistTemps.length){ 152 | let i1 = params.gh.findIndex(e=> e>moistGhs[1]),i2=i1-1; 153 | if (i2>0){ 154 | let tempIp = math.linearInterpolate(params.gh[i1],params.temp[i1],params.gh[i2],params.temp[i2],moistGhs[1]); 155 | if (moistTemps[1] < tempIp){ 156 | if (!equilibrium){ 157 | //console.log("%c no Equilibrium found, cut moist temp line short","color:green"); 158 | //no intersection found, so use point one as the end. 159 | equilibrium = [moistGhs[1], moistTemps[1]]; 160 | 161 | } 162 | } 163 | } 164 | } 165 | if (equilibrium) { 166 | cloudTop = equilibrium[0]; 167 | pCloudTop = pToEl.invert(equilibrium[0]); 168 | moist = moist.filter((pt) => pt[1] >= pCloudTop); 169 | moist.push([equilibrium[1], pCloudTop]); 170 | } else { //does not intersect, very high CBs 171 | cloudTop = 100000; 172 | pCloudTop = pToEl.invert(cloudTop); 173 | } 174 | return { moist, cloudTop, pCloudTop }; 175 | } 176 | 177 | 178 | for (let elevation = minEl; elevation <= maxEl; elevation += stepEl) { 179 | const p = pToEl.invert(elevation); 180 | const t = dryLapse(p, sfcT, sfcP); 181 | const dp = dewpoint(vaporPressure(p, mRatio)); 182 | dryGhs.push(elevation); 183 | dryPressures.push(p); 184 | dryTemps.push(t); //dry adiabat line from templine surfc 185 | dryDewpoints.push(dp); //isohume line from dewpoint line surfc 186 | 187 | const t2 = dryLapse(p, params.temp[0], sfcP); 188 | dryTempsTempline.push(t2); 189 | } 190 | 191 | const cloudBase = math.firstIntersection(dryGhs, dryTemps, dryGhs, dryDewpoints); 192 | //intersection dry adiabat from surface temp to isohume from surface dewpoint, if dp==surf temp, then cloudBase will be null 193 | 194 | let thermalTop = math.firstIntersection(dryGhs, dryTemps, params.gh, params.temp); 195 | //intersection of dryadiabat from surface to templine. this will be null if stable, leaning to the right 196 | 197 | let LCL = math.firstIntersection(dryGhs, dryTempsTempline, dryGhs, dryDewpoints); 198 | //intersection dry adiabat from surface temp to isohume from surface dewpoint, if dp==surf temp, then cloudBase will be null 199 | 200 | let CCL = math.firstIntersection(dryGhs, dryDewpoints, params.gh, params.temp); 201 | //console.log(CCL, dryGhs, dryDewpoints, params.gh, params.temp ); 202 | //intersection of isohume line with templine 203 | 204 | 205 | //console.log(cloudBase, thermalTop, LCL, CCL); 206 | 207 | if (LCL && LCL.length) { 208 | parcel.LCL = LCL[0]; 209 | let LCLp = pToEl.invert(LCL[0]); 210 | parcel.isohumeToDry = [].concat( 211 | math.zip(dryTempsTempline, dryPressures).filter(p => p[1] >= LCLp), 212 | [[LCL[1], LCLp]], 213 | math.zip(dryDewpoints, dryPressures).filter(p => p[1] >= LCLp).reverse() 214 | ); 215 | } 216 | 217 | if (CCL && CCL.length) { 218 | //parcel.CCL=CCL[0]; 219 | let CCLp = pToEl.invert(CCL[0]); 220 | parcel.TCON = dryLapse(sfcP, CCL[1], CCLp); 221 | 222 | //check if dryTempsTCON crosses temp line at CCL, if lower, then inversion exists and TCON, must be moved. 223 | 224 | //console.log(parcel.TCON) 225 | let dryTempsTCON=[]; 226 | 227 | for(let CCLtempMoreThanTempLine=false; !CCLtempMoreThanTempLine; parcel.TCON+=0.5){ 228 | 229 | let crossTemp = [-Infinity]; 230 | 231 | for (; crossTemp[0] < CCL[0]; parcel.TCON += 0.5) { 232 | //if (crossTemp[0]!=-Infinity) console.log("TCON MUST BE MOVED"); 233 | dryTempsTCON = []; 234 | for (let elevation = minEl; elevation <= maxEl; elevation += stepEl) { //line from isohume/temp intersection to TCON 235 | const t = dryLapse(pToEl.invert(elevation), parcel.TCON, sfcP); 236 | dryTempsTCON.push(t); 237 | } 238 | crossTemp = math.firstIntersection(dryGhs, dryTempsTCON, params.gh, params.temp) || [-Infinity]; //intersection may return null 239 | 240 | 241 | } 242 | 243 | parcel.TCON -= 0.5; 244 | 245 | if (crossTemp[0] > CCL[0]) { 246 | CCL = math.firstIntersection(dryGhs, dryTempsTCON, dryGhs, dryDewpoints); 247 | //now check if temp at CCL is more than temp line, if not, has hit another inversion and parcel.TCON must be moved further 248 | let i2= params.gh.findIndex(gh => gh>CCL[0]), i1= i2-1; 249 | if (i1>=0){ 250 | let tempLineIp=math.linearInterpolate(params.gh[i1], params.temp[i1], params.gh[i2], params.temp[i2], CCL[0]); 251 | if (CCL[1] > tempLineIp) { 252 | CCLtempMoreThanTempLine = true; 253 | //console.log("%c CCL1 is more than templine", "color:green", CCL[1], tempLineIp); 254 | } else { 255 | //console.log("%c CCL1 is less than tempLine "+ "%c move TCON", "color:green","color:red") 256 | } 257 | } 258 | } 259 | } 260 | parcel.TCON -= 0.5; 261 | 262 | 263 | parcel.CCL = CCL[0]; 264 | CCLp = pToEl.invert(CCL[0]); 265 | 266 | parcel.isohumeToTemp = [].concat( 267 | math.zip(dryDewpoints, dryPressures).filter(p => p[1] >= CCLp), 268 | [[CCL[1], CCLp]], 269 | math.zip(dryTempsTCON, dryPressures).filter(p => p[1] >= CCLp).reverse() 270 | ); 271 | parcel.moistFromCCL = moistLineFromEandT(CCL[0], CCL[1]).moist; 272 | } 273 | 274 | parcel.surface = params.gh[0]; 275 | 276 | 277 | if (!thermalTop) { 278 | return parcel; 279 | } else { 280 | parcel.origThermalTop = thermalTop[0]; 281 | } 282 | 283 | if (thermalTop && cloudBase && cloudBase[0] < thermalTop[0]) { 284 | 285 | thermalTop = cloudBase; 286 | 287 | const pCloudBase = pToEl.invert(cloudBase[0]); 288 | 289 | Object.assign( 290 | parcel, 291 | moistLineFromEandT(cloudBase[0], cloudBase[1]) //add to parcel: moist = [[moistTemp,moistP]...], cloudTop and pCloudTop. 292 | ); 293 | 294 | const isohume = math.zip(dryDewpoints, dryPressures).filter((pt) => pt[1] > pCloudBase); //filter for pressures higher than cloudBase, thus lower than cloudBase 295 | isohume.push([cloudBase[1], pCloudBase]); 296 | 297 | 298 | 299 | //parcel.pCloudTop = params.level[params.level.length - 1]; 300 | 301 | 302 | 303 | //parcel.cloudTop = cloudTop; 304 | //parcel.pCloudTop = pCloudTop; 305 | 306 | //parcel.moist = moist; 307 | 308 | parcel.isohume = isohume; 309 | 310 | } 311 | 312 | let pThermalTop = pToEl.invert(thermalTop[0]); 313 | const dry = math.zip(dryTemps, dryPressures).filter((pt) => pt[1] > pThermalTop); 314 | dry.push([thermalTop[1], pThermalTop]); 315 | 316 | parcel.dry = dry; 317 | parcel.pThermalTop = pThermalTop; 318 | parcel.elevThermalTop = thermalTop[0]; 319 | 320 | 321 | 322 | //console.log(parcel); 323 | return parcel; 324 | } 325 | 326 | export default { 327 | dryLapse, 328 | mixingRatio, 329 | saturationVaporPressure, 330 | moistGradientT, 331 | vaporPressure, 332 | dewpoint, 333 | getElevation, 334 | getElevation2, 335 | pressureFromElevation, 336 | getSurfaceP, 337 | parcelTrajectory, 338 | } -------------------------------------------------------------------------------- /src/clouds.mjs: -------------------------------------------------------------------------------- 1 | function lerp(v0, v1, weight) { 2 | return v0 + weight * (v1 - v0); 3 | } 4 | 5 | let _hrAlt = [0, 5, 11, 16.7, 25, 33.4, 50, 58.4, 66.7, 75, 83.3, 92, 98, 100]; //wndy distribution % for pressure levels 6 | 7 | 8 | 9 | /////////to test 10 | _hrAlt = [0, 3.42, 4.82, 6.26, 9.23, 12.34, 19.07, 26.63, 35.29, 45.49, 58.01, 74.54, 85.51, 100]; 11 | //based on standard elevation for pressure levels below, thus linear for elevation 12 | 13 | //using d3, basep=1050 14 | _hrAlt = [2.07, 4.26, 5.39, 6.56, 8.99, 11.56, 17.24, 23.80, 31.55, 41.04, 53.28, 70.52, 82.75, 100]; 15 | 16 | var _hrAltPressure = [null, 950, 925, 900, 850, 800, 700, 600, 500, 400, 300, 200, 150, null]; 17 | ///// 18 | 19 | 20 | 21 | 22 | const lookup = new Uint8Array(256); 23 | 24 | for (let i = 0; i < 160; i++) { 25 | lookup[i] = clampIndex(24 * Math.floor((i + 12) / 16), 160); 26 | } 27 | 28 | 29 | 30 | // Compute the rain clouds cover. 31 | // Output an object: 32 | // - clouds: the clouds cover, 33 | // - width & height: dimension of the cover data. 34 | function computeClouds(ad, wdth = 1, hght = 200) { ////added wdth and hght, to improve performance ///supply own hrAlt altutude percentage distribution, based on pressure levels 35 | // Compute clouds data. 36 | 37 | //console.log("WID",wdth,hght); 38 | 39 | /////////convert to windy format 40 | //ad must be sorted; 41 | 42 | const logscale = (x, d, r) => { //log scale function D3, x is the value d is the domain [] and r is the range [] 43 | let xlog = Math.log10(x), 44 | dlog = [Math.log10(d[0]), Math.log10(d[1])], 45 | delta_d = dlog[1] - dlog[0], 46 | delta_r = r[1] - r[0]; 47 | return r[0] + ((xlog - dlog[0]) / delta_d) * delta_r; 48 | } 49 | 50 | let airData = {}; 51 | let hrAltPressure = [], hrAlt = []; 52 | ad.forEach(a => { 53 | if (!a.press) return; 54 | if (a.rh == void 0 && a.dwpt && a.temp) { 55 | a.rh = 100 * (Math.exp((17.625 * a.dwpt) / (243.04 + a.dwpt)) / Math.exp((17.625 * a.temp) / (243.04 + a.temp))); ///August-Roche-Magnus approximation. 56 | } 57 | if (a.rh && a.press >= 100) { 58 | let p = Math.round(a.press); 59 | airData[`rh-${p}h`] = [a.rh]; 60 | hrAltPressure.push(p); 61 | hrAlt.push(logscale(p, [1050, 100], [0, 100])); 62 | } 63 | }) 64 | 65 | //fi x underground clouds, add humidty 0 element in airData wehre the pressure is surfcace pressure +1: 66 | airData[`rh-${(hrAltPressure[0] + 1)}h`] = [0]; 67 | hrAlt.unshift(null, hrAlt[0]); 68 | hrAltPressure.unshift(null, hrAltPressure[0] + 1); 69 | hrAltPressure.pop(); hrAltPressure.push(null); 70 | 71 | /////////// 72 | 73 | 74 | const numX = airData[`rh-${hrAltPressure[1]}h`].length; 75 | const numY = hrAltPressure.length; 76 | const rawClouds = new Array(numX * numY); 77 | 78 | for (let y = 0, index = 0; y < numY; ++y) { 79 | if (hrAltPressure[y] == null) { 80 | for (let x = 0; x < numX; ++x) { 81 | rawClouds[index++] = 0.0; 82 | } 83 | } else { 84 | const weight = hrAlt[y] * 0.01; 85 | const pAdd = lerp(-60, -70, weight); 86 | const pMul = lerp(0.025, 0.038, weight); 87 | const pPow = lerp(6, 4, weight); 88 | const pMul2 = 1 - 0.8 * Math.pow(weight, 0.7); 89 | const rhRow = airData[`rh-${hrAltPressure[y]}h`]; 90 | for (let x = 0; x < numX; ++x) { 91 | const hr = Number(rhRow[x]); 92 | let f = Math.max(0.0, Math.min((hr + pAdd) * pMul, 1.0)); 93 | f = Math.pow(f, pPow) * pMul2; 94 | rawClouds[index++] = f; 95 | } 96 | } 97 | } 98 | 99 | 100 | // Interpolate raw clouds. 101 | const sliceWidth = wdth || 10; 102 | const width = sliceWidth * numX; 103 | const height = hght || 300; 104 | const clouds = new Array(width * height); 105 | const kh = (height - 1) * 0.01; 106 | const dx2 = (sliceWidth + 1) >> 1; 107 | let heightLookupIndex = 2 * height; 108 | const heightLookup = new Array(heightLookupIndex); 109 | const buffer = new Array(16); 110 | let previousY; 111 | let currentY = height; 112 | 113 | for (let j = 0; j < numY - 1; ++j) { 114 | previousY = currentY; 115 | currentY = Math.round(height - 1 - hrAlt[j + 1] * kh); 116 | const j0 = numX * clampIndex(j + 2, numY); 117 | const j1 = numX * clampIndex(j + 1, numY); 118 | const j2 = numX * clampIndex(j + 0, numY); 119 | const j3 = numX * clampIndex(j - 1, numY); 120 | let previousX = 0; 121 | let currentX = dx2; 122 | const deltaY = previousY - currentY; 123 | const invDeltaY = 1.0 / deltaY; 124 | 125 | for (let i = 0; i < numX + 1; ++i) { 126 | if (i == 0 && deltaY > 0) { 127 | const ry = 1.0 / deltaY; 128 | for (let l = 0; l < deltaY; l++) { 129 | heightLookup[--heightLookupIndex] = j; 130 | heightLookup[--heightLookupIndex] = Math.round(10000 * ry * l); 131 | } 132 | } 133 | const i0 = clampIndex(i - 2, numX); 134 | const i1 = clampIndex(i - 1, numX); 135 | const i2 = clampIndex(i + 0, numX); 136 | const i3 = clampIndex(i + 1, numX); 137 | buffer[0] = rawClouds[j0 + i0]; 138 | buffer[1] = rawClouds[j0 + i1]; 139 | buffer[2] = rawClouds[j0 + i2]; 140 | buffer[3] = rawClouds[j0 + i3]; 141 | buffer[4] = rawClouds[j1 + i0]; 142 | buffer[5] = rawClouds[j1 + i1]; 143 | buffer[6] = rawClouds[j1 + i2]; 144 | buffer[7] = rawClouds[j1 + i3]; 145 | buffer[8] = rawClouds[j2 + i0]; 146 | buffer[9] = rawClouds[j2 + i1]; 147 | buffer[10] = rawClouds[j2 + i2]; 148 | buffer[11] = rawClouds[j2 + i3]; 149 | buffer[12] = rawClouds[j3 + i0]; 150 | buffer[13] = rawClouds[j3 + i1]; 151 | buffer[14] = rawClouds[j3 + i2]; 152 | buffer[15] = rawClouds[j3 + i3]; 153 | 154 | const topLeft = currentY * width + previousX; 155 | const dx = currentX - previousX; 156 | const fx = 1.0 / dx; 157 | 158 | for (let y = 0; y < deltaY; ++y) { 159 | let offset = topLeft + y * width; 160 | for (let x = 0; x < dx; ++x) { 161 | const black = step(bicubicFiltering(buffer, fx * x, invDeltaY * y) * 160.0); 162 | clouds[offset++] = 255 - black; 163 | } 164 | } 165 | 166 | previousX = currentX; 167 | currentX += sliceWidth; 168 | 169 | if (currentX > width) { 170 | currentX = width; 171 | } 172 | } 173 | } 174 | 175 | return { clouds, width, height }; 176 | } 177 | 178 | function clampIndex(index, size) { 179 | return index < 0 ? 0 : index > size - 1 ? size - 1 : index; 180 | } 181 | 182 | function step(x) { 183 | return lookup[Math.floor(clampIndex(x, 160))]; 184 | } 185 | 186 | function cubicInterpolate(y0, y1, y2, y3, m) { 187 | const a0 = -y0 * 0.5 + 3.0 * y1 * 0.5 - 3.0 * y2 * 0.5 + y3 * 0.5; 188 | const a1 = y0 - 5.0 * y1 * 0.5 + 2.0 * y2 - y3 * 0.5; 189 | const a2 = -y0 * 0.5 + y2 * 0.5; 190 | return a0 * m ** 3 + a1 * m ** 2 + a2 * m + y1; 191 | } 192 | 193 | function bicubicFiltering(m, s, t) { 194 | return cubicInterpolate( 195 | cubicInterpolate(m[0], m[1], m[2], m[3], s), 196 | cubicInterpolate(m[4], m[5], m[6], m[7], s), 197 | cubicInterpolate(m[8], m[9], m[10], m[11], s), 198 | cubicInterpolate(m[12], m[13], m[14], m[15], s), 199 | t 200 | ); 201 | } 202 | 203 | // Draw the clouds on a canvas. 204 | // This function is useful for debugging. 205 | function cloudsToCanvas({ clouds, width, height, canvas }) { 206 | if (canvas == null) { 207 | canvas = document.createElement("canvas"); 208 | } 209 | canvas.width = width; 210 | canvas.height = height; 211 | const ctx = canvas.getContext("2d"); 212 | let imageData = ctx.getImageData(0, 0, width, height); 213 | let imgData = imageData.data; 214 | 215 | 216 | let srcOffset = 0; 217 | let dstOffset = 0; 218 | for (let x = 0; x < width; ++x) { 219 | for (let y = 0; y < height; ++y) { 220 | const color = clouds[srcOffset++]; 221 | imgData[dstOffset++] = color; 222 | imgData[dstOffset++] = color; 223 | imgData[dstOffset++] = color; 224 | imgData[dstOffset++] = color < 245 ? 255 : 0; 225 | } 226 | } 227 | 228 | 229 | ctx.putImageData(imageData, 0, 0); 230 | ctx.drawImage(canvas, 0, 0, width, height); 231 | 232 | return canvas; 233 | } 234 | 235 | export default { 236 | computeClouds, 237 | cloudsToCanvas 238 | } -------------------------------------------------------------------------------- /src/d3.custom.min.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | //custom built d3 version 5.16. 4 | 5 | //!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){ 6 | "use strict"; 7 | const t={}; 8 | function n(t,n){return tn?1:t>=n?0:NaN}function e(t){var e;return 1===t.length&&(e=t,t=function(t,r){return n(e(t),r)}),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}var r=e(n),i=r.right;r.left;var o=Math.sqrt(50),a=Math.sqrt(10),u=Math.sqrt(2);function s(t,n,e){var r,i,o,a,u=-1;if(e=+e,(t=+t)===(n=+n)&&e>0)return[t];if((r=n0)for(t=Math.ceil(t/a),n=Math.floor(n/a),o=new Array(i=Math.ceil(n-t+1));++u=0?(s>=o?10:s>=a?5:s>=u?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(s>=o?10:s>=a?5:s>=u?2:1)}var c=Array.prototype.slice;function h(t){return t}var f=1e-6;function p(t){return"translate("+(t+.5)+",0)"}function d(t){return"translate(0,"+(t+.5)+")"}function g(t){return function(n){return+t(n)}}function v(t){var n=Math.max(0,t.bandwidth()-1)/2;return t.round()&&(n=Math.round(n)),function(e){return+t(e)+n}}function m(){return!this.__axis}function y(t,n){var e=[],r=null,i=null,o=6,a=6,u=3,s=1===t||4===t?-1:1,l=4===t||2===t?"x":"y",y=1===t||3===t?p:d;function _(c){var p=null==r?n.ticks?n.ticks.apply(n,e):n.domain():r,d=null==i?n.tickFormat?n.tickFormat.apply(n,e):h:i,_=Math.max(o,0)+u,w=n.range(),x=+w[0]+.5,b=+w[w.length-1]+.5,M=(n.bandwidth?v:g)(n.copy()),k=c.selection?c.selection():c,N=k.selectAll(".domain").data([null]),A=k.selectAll(".tick").data(p,n).order(),E=A.exit(),S=A.enter().append("g").attr("class","tick"),T=A.select("line"),P=A.select("text");N=N.merge(N.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),A=A.merge(S),T=T.merge(S.append("line").attr("stroke","currentColor").attr(l+"2",s*o)),P=P.merge(S.append("text").attr("fill","currentColor").attr(l,s*_).attr("dy",1===t?"0em":3===t?"0.71em":"0.32em")),c!==k&&(N=N.transition(c),A=A.transition(c),T=T.transition(c),P=P.transition(c),E=E.transition(c).attr("opacity",f).attr("transform",(function(t){return isFinite(t=M(t))?y(t):this.getAttribute("transform")})),S.attr("opacity",f).attr("transform",(function(t){var n=this.parentNode.__axis;return y(n&&isFinite(n=n(t))?n:M(t))}))),E.remove(),N.attr("d",4===t||2==t?a?"M"+s*a+","+x+"H0.5V"+b+"H"+s*a:"M0.5,"+x+"V"+b:a?"M"+x+","+s*a+"V0.5H"+b+"V"+s*a:"M"+x+",0.5H"+b),A.attr("opacity",1).attr("transform",(function(t){return y(M(t))})),T.attr(l+"2",s*o),P.attr(l,s*_).text(d),k.filter(m).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",2===t?"start":4===t?"end":"middle"),k.each((function(){this.__axis=M}))}return _.scale=function(t){return arguments.length?(n=t,_):n},_.ticks=function(){return e=c.call(arguments),_},_.tickArguments=function(t){return arguments.length?(e=null==t?[]:c.call(t),_):e.slice()},_.tickValues=function(t){return arguments.length?(r=null==t?null:c.call(t),_):r&&r.slice()},_.tickFormat=function(t){return arguments.length?(i=t,_):i},_.tickSize=function(t){return arguments.length?(o=a=+t,_):o},_.tickSizeInner=function(t){return arguments.length?(o=+t,_):o},_.tickSizeOuter=function(t){return arguments.length?(a=+t,_):a},_.tickPadding=function(t){return arguments.length?(u=+t,_):u},_}var _={value:function(){}};function w(){for(var t,n=0,e=arguments.length,r={};n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}}))}function M(t,n){for(var e,r=0,i=t.length;r0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),A.hasOwnProperty(n)?{space:A[n],local:t}:t}function S(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===N&&n.documentElement.namespaceURI===N?n.createElement(t):n.createElementNS(e,t)}}function T(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function P(t){var n=E(t);return(n.local?T:S)(n)}function C(){}function q(t){return null==t?C:function(){return this.querySelector(t)}}function z(){return[]}function L(t){return null==t?z:function(){return this.querySelectorAll(t)}}function j(t){return function(){return this.matches(t)}}function X(t){return new Array(t.length)}function O(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}O.prototype={constructor:O,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};function V(t,n,e,r,i,o){for(var a,u=0,s=n.length,l=o.length;un?1:t>=n?0:NaN}function D(t){return function(){this.removeAttribute(t)}}function $(t){return function(){this.removeAttributeNS(t.space,t.local)}}function H(t,n){return function(){this.setAttribute(t,n)}}function F(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function Y(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function B(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function U(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function G(t){return function(){this.style.removeProperty(t)}}function Z(t,n,e){return function(){this.style.setProperty(t,n,e)}}function K(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function Q(t,n){return t.style.getPropertyValue(n)||U(t).getComputedStyle(t,null).getPropertyValue(n)}function J(t){return function(){delete this[t]}}function W(t,n){return function(){this[t]=n}}function tt(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function nt(t){return t.trim().split(/^|\s+/)}function et(t){return t.classList||new rt(t)}function rt(t){this._node=t,this._names=nt(t.getAttribute("class")||"")}function it(t,n){for(var e=et(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var xt={},bt=null;"undefined"!=typeof document&&("onmouseenter"in document.documentElement||(xt={mouseenter:"mouseover",mouseleave:"mouseout"}));function Mt(t,n,e){return t=kt(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n)}}function kt(t,n,e){return function(r){var i=bt;bt=r;try{t.call(this,this.__data__,n,e)}finally{bt=i}}}function Nt(t){return t.trim().split(/^|\s+/).map((function(t){var n="",e=t.indexOf(".");return e>=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}function At(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=w&&(w=_+1);!(y=v[w])&&++w=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=I);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?G:"function"==typeof n?K:Z)(t,n,null==e?"":e)):Q(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?J:"function"==typeof n?tt:W)(t,n)):this.node()[t]},classed:function(t,n){var e=nt(t+"");if(arguments.length<2){for(var r=et(this.node()),i=-1,o=e.length;++i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?vn(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?vn(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=on.exec(t))?new _n(n[1],n[2],n[3],1):(n=an.exec(t))?new _n(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=un.exec(t))?vn(n[1],n[2],n[3],n[4]):(n=sn.exec(t))?vn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=ln.exec(t))?Mn(n[1],n[2]/100,n[3]/100,1):(n=cn.exec(t))?Mn(n[1],n[2]/100,n[3]/100,n[4]):hn.hasOwnProperty(t)?gn(hn[t]):"transparent"===t?new _n(NaN,NaN,NaN,0):null}function gn(t){return new _n(t>>16&255,t>>8&255,255&t,1)}function vn(t,n,e,r){return r<=0&&(t=n=e=NaN),new _n(t,n,e,r)}function mn(t){return t instanceof Qt||(t=dn(t)),t?new _n((t=t.rgb()).r,t.g,t.b,t.opacity):new _n}function yn(t,n,e,r){return 1===arguments.length?mn(t):new _n(t,n,e,null==r?1:r)}function _n(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function wn(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)}function xn(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function bn(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function Mn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Nn(t,n,e,r)}function kn(t){if(t instanceof Nn)return new Nn(t.h,t.s,t.l,t.opacity);if(t instanceof Qt||(t=dn(t)),!t)return new Nn;if(t instanceof Nn)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,s=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&s<1?0:a,new Nn(a,u,s,t.opacity)}function Nn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function An(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function En(t){return function(){return t}}function Sn(t){return 1==(t=+t)?Tn:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):En(isNaN(n)?e:n)}}function Tn(t,n){var e=n-t;return e?function(t,n){return function(e){return t+e*n}}(t,e):En(isNaN(t)?n:t)}Zt(Qt,dn,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:fn,formatHex:fn,formatHsl:function(){return kn(this).formatHsl()},formatRgb:pn,toString:pn}),Zt(_n,yn,Kt(Qt,{brighter:function(t){return t=null==t?Wt:Math.pow(Wt,t),new _n(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Jt:Math.pow(Jt,t),new _n(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:wn,formatHex:wn,formatRgb:xn,toString:xn})),Zt(Nn,(function(t,n,e,r){return 1===arguments.length?kn(t):new Nn(t,n,e,null==r?1:r)}),Kt(Qt,{brighter:function(t){return t=null==t?Wt:Math.pow(Wt,t),new Nn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Jt:Math.pow(Jt,t),new Nn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new _n(An(t>=240?t-240:t+120,i,r),An(t,i,r),An(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));var Pn=function t(n){var e=Sn(n);function r(t,n){var r=e((t=yn(t)).r,(n=yn(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=Tn(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function Cn(t,n){n||(n=[]);var e,r=t?Math.min(n.length,t.length):0,i=n.slice();return function(o){for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,s.push({i:a,x:Ln(e,r)})),o=On.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Ln(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,s),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Ln(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,s),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:Ln(t,e)},{i:u-2,x:Ln(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,s),o=a=null,function(t){for(var n,e=-1,r=s.length;++e=0&&n._call.call(null,t),n=n._next;--Wn}()}finally{Wn=0,function(){var t,n,e=Zn,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:Zn=n);Kn=t,pe(r)}(),re=0}}function fe(){var t=oe.now(),n=t-ee;n>1e3&&(ie-=n,ee=t)}function pe(t){Wn||(te&&(te=clearTimeout(te)),t-re>24?(t<1/0&&(te=setTimeout(he,t-oe.now()-ie)),ne&&(ne=clearInterval(ne))):(ne||(ee=oe.now(),ne=setInterval(fe,1e3)),Wn=1,ae(he)))}function de(t,n,e){var r=new le;return n=null==n?0:+n,r.restart((function(e){r.stop(),t(e+n)}),n,e),r}le.prototype=ce.prototype={constructor:le,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?ue():+e)+(null==n?0:+n),this._next||Kn===this||(Kn?Kn._next=this:Zn=this,Kn=this),this._call=t,this._time=e,pe()},stop:function(){this._call&&(this._call=null,this._time=1/0,pe())}};var ge=w("start","end","cancel","interrupt"),ve=[];function me(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=1,e.timer.restart(a,e.delay,e.time),e.delay<=t&&a(t-e.delay)}function a(o){var l,c,h,f;if(1!==e.state)return s();for(l in i)if((f=i[l]).name===e.name){if(3===f.state)return de(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[l]):+l0)throw new Error("too late; already scheduled");return e}function _e(t,n){var e=we(t,n);if(e.state>3)throw new Error("too late; already running");return e}function we(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function xe(t,n){var e,r;return function(){var i=_e(this,t),o=i.tween;if(o!==e)for(var a=0,u=(r=e=o).length;a=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?ye:_e;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}var De=Lt.prototype.constructor;function $e(t){return function(){this.style.removeProperty(t)}}function He(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}function Fe(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&He(t,o,e)),r}return o._value=n,o}function Ye(t){return function(n){this.textContent=t.call(this,n)}}function Be(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&Ye(r)),n}return r._value=t,r}var Ue=0;function Ge(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function Ze(){return++Ue}var Ke=Lt.prototype;Ge.prototype={constructor:Ge,select:function(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=q(t));for(var r=this._groups,i=r.length,o=new Array(i),a=0;a2&&e.state<5,e.state=6,e.timer.stop(),e.on.call(r?"interrupt":"cancel",t,t.__data__,e.index,e.group),delete o[i]):a=!1;a&&delete t.__transition}}(this,t)}))},Lt.prototype.transition=function(t){var n,e;t instanceof Ge?(n=t._id,t=t._name):(n=Ze(),(e=Qe).time=ue(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,o=0;onr)if(Math.abs(c*u-s*l)>nr&&i){var f=e-o,p=r-a,d=u*u+s*s,g=f*f+p*p,v=Math.sqrt(d),m=Math.sqrt(h),y=i*Math.tan((We-Math.acos((d+h-g)/(2*v*m)))/2),_=y/m,w=y/v;Math.abs(_-1)>nr&&(this._+="L"+(t+_*l)+","+(n+_*c)),this._+="A"+i+","+i+",0,0,"+ +(c*f>l*p)+","+(this._x1=t+w*u)+","+(this._y1=n+w*s)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,o=!!o;var a=(e=+e)*Math.cos(r),u=e*Math.sin(r),s=t+a,l=n+u,c=1^o,h=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+s+","+l:(Math.abs(this._x1-s)>nr||Math.abs(this._y1-l)>nr)&&(this._+="L"+s+","+l),e&&(h<0&&(h=h%tr+tr),h>er?this._+="A"+e+","+e+",0,1,"+c+","+(t-a)+","+(n-u)+"A"+e+","+e+",0,1,"+c+","+(this._x1=s)+","+(this._y1=l):h>nr&&(this._+="A"+e+","+e+",0,"+ +(h>=We)+","+c+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};var or="$";function ar(){}function ur(t,n){var e=new ar;if(t instanceof ar)t.each((function(t,n){e.set(n,t)}));else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++i1?r[0]+r.slice(2):r,+t.slice(e+1)]}function hr(t){return(t=cr(Math.abs(t)))?t[1]:NaN}sr.prototype={constructor:sr,has:lr.has,add:function(t){return this[or+(t+="")]=t,this},remove:lr.remove,clear:lr.clear,values:lr.keys,size:lr.size,empty:lr.empty,each:lr.each};var fr,pr=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function dr(t){if(!(n=pr.exec(t)))throw new Error("invalid format: "+t);var n;return new gr({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function gr(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function vr(t,n){var e=cr(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}dr.prototype=gr.prototype,gr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var mr={"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return vr(100*t,n)},r:vr,s:function(t,n){var e=cr(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(fr=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+cr(t,Math.max(0,n+o-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}};function yr(t){return t}var _r,wr,xr=Array.prototype.map,br=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function Mr(t){var n,e,r=void 0===t.grouping||void 0===t.thousands?yr:(n=xr.call(t.grouping,Number),e=t.thousands+"",function(t,r){for(var i=t.length,o=[],a=0,u=n[0],s=0;i>0&&u>0&&(s+u+1>r&&(u=Math.max(1,r-s)),o.push(t.substring(i-=u,i+u)),!((s+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}),i=void 0===t.currency?"":t.currency[0]+"",o=void 0===t.currency?"":t.currency[1]+"",a=void 0===t.decimal?".":t.decimal+"",u=void 0===t.numerals?yr:function(t){return function(n){return n.replace(/[0-9]/g,(function(n){return t[+n]}))}}(xr.call(t.numerals,String)),s=void 0===t.percent?"%":t.percent+"",l=void 0===t.minus?"-":t.minus+"",c=void 0===t.nan?"NaN":t.nan+"";function h(t){var n=(t=dr(t)).fill,e=t.align,h=t.sign,f=t.symbol,p=t.zero,d=t.width,g=t.comma,v=t.precision,m=t.trim,y=t.type;"n"===y?(g=!0,y="g"):mr[y]||(void 0===v&&(v=12),m=!0,y="g"),(p||"0"===n&&"="===e)&&(p=!0,n="0",e="=");var _="$"===f?i:"#"===f&&/[boxX]/.test(y)?"0"+y.toLowerCase():"",w="$"===f?o:/[%p]/.test(y)?s:"",x=mr[y],b=/[defgprs%]/.test(y);function M(t){var i,o,s,f=_,M=w;if("c"===y)M=x(t)+M,t="";else{var k=(t=+t)<0||1/t<0;if(t=isNaN(t)?c:x(Math.abs(t),v),m&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),k&&0==+t&&"+"!==h&&(k=!1),f=(k?"("===h?h:l:"-"===h||"("===h?"":h)+f,M=("s"===y?br[8+fr/3]:"")+M+(k&&"("===h?")":""),b)for(i=-1,o=t.length;++i(s=t.charCodeAt(i))||s>57){M=(46===s?a+t.slice(i+1):t.slice(i))+M,t=t.slice(0,i);break}}g&&!p&&(t=r(t,1/0));var N=f.length+t.length+M.length,A=N>1)+f+t+M+A.slice(N);break;default:t=A+f+t+M}return u(t)}return v=void 0===v?6:/[gprs]/.test(y)?Math.max(1,Math.min(21,v)):Math.max(0,Math.min(20,v)),M.toString=function(){return t+""},M}return{format:h,formatPrefix:function(t,n){var e=h(((t=dr(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(hr(n)/3))),i=Math.pow(10,-r),o=br[8+r/3];return function(t){return e(i*t)+o}}}}function kr(t,n){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(n).domain(t)}return this}t.format=void 0,_r=Mr({decimal:".",thousands:",",grouping:[3],currency:["$",""],minus:"-"}),t.format=_r.format,wr=_r.formatPrefix;var Nr=Array.prototype,Ar=Nr.map,Er=Nr.slice;function Sr(t){return+t}var Tr=[0,1];function Pr(t){return t}function Cr(t,n){return(n-=t=+t)?function(e){return(e-t)/n}:function(t){return function(){return t}}(isNaN(n)?NaN:.5)}function qr(t){var n,e=t[0],r=t[t.length-1];return e>r&&(n=e,e=r,r=n),function(t){return Math.max(e,Math.min(r,t))}}function zr(t,n,e){var r=t[0],i=t[1],o=n[0],a=n[1];return i2?Lr:zr,i=o=null,h}function h(n){return isNaN(n=+n)?e:(i||(i=r(a.map(t),u,s)))(t(l(n)))}return h.invert=function(e){return l(n((o||(o=r(u,a.map(t),Ln)))(e)))},h.domain=function(t){return arguments.length?(a=Ar.call(t,Sr),l===Pr||(l=qr(a)),c()):a.slice()},h.range=function(t){return arguments.length?(u=Er.call(t),c()):u.slice()},h.rangeRound=function(t){return u=Er.call(t),s=In,c()},h.clamp=function(t){return arguments.length?(l=t?qr(a):Pr,h):l!==Pr},h.interpolate=function(t){return arguments.length?(s=t,c()):s},h.unknown=function(t){return arguments.length?(e=t,h):e},function(e,r){return t=e,n=r,c()}}function Or(t,n){return Xr()(t,n)}function Vr(n,e,r,i){var s,l=function(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),s=r/i;return s>=o?i*=10:s>=a?i*=5:s>=u&&(i*=2),n0?r=l(u=Math.floor(u/r)*r,s=Math.ceil(s/r)*r,e):r<0&&(r=l(u=Math.ceil(u*r)/r,s=Math.floor(s*r)/r,e)),r>0?(i[o]=Math.floor(u/r)*r,i[a]=Math.ceil(s/r)*r,n(i)):r<0&&(i[o]=Math.ceil(u*r)/r,i[a]=Math.floor(s*r)/r,n(i)),t},t}function Ir(t){return Math.log(t)}function Dr(t){return Math.exp(t)}function $r(t){return-Math.log(-t)}function Hr(t){return-Math.exp(-t)}function Fr(t){return isFinite(t)?+("1e"+t):t<0?0:t}function Yr(t){return function(n){return-t(-n)}}function Br(n){var e,r,i=n(Ir,Dr),o=i.domain,a=10;function u(){return e=function(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),function(n){return Math.log(n)/t})}(a),r=function(t){return 10===t?Fr:t===Math.E?Math.exp:function(n){return Math.pow(t,n)}}(a),o()[0]<0?(e=Yr(e),r=Yr(r),n($r,Hr)):n(Ir,Dr),i}return i.base=function(t){return arguments.length?(a=+t,u()):a},i.domain=function(t){return arguments.length?(o(t),u()):o()},i.ticks=function(t){var n,i=o(),u=i[0],l=i[i.length-1];(n=l0){for(;pl)break;v.push(f)}}else for(;p=1;--h)if(!((f=c*h)l)break;v.push(f)}}else v=s(p,d,Math.min(d-p,g)).map(r);return n?v.reverse():v},i.tickFormat=function(n,o){if(null==o&&(o=10===a?".0e":","),"function"!=typeof o&&(o=t.format(o)),n===1/0)return o;null==n&&(n=10);var u=Math.max(1,a*n/i.ticks().length);return function(t){var n=t/r(Math.round(e(t)));return n*ah}s.mouse("drag")}function g(){jt(bt.view).on("mousemove.drag mouseup.drag",null),function(t,n){var e=t.document.documentElement,r=jt(t).on("dragstart.drag",null);n&&(r.on("click.drag",Dt,!0),setTimeout((function(){r.on("click.drag",null)}),0)),"onselectstart"in e?r.on("selectstart.drag",null):(e.style.MozUserSelect=e.__noselect,delete e.__noselect)}(bt.view,e),Dt(),s.mouse("end")}function v(){if(i.apply(this,arguments)){var t,n,e=bt.changedTouches,r=o.apply(this,arguments),a=e.length;for(t=0;t y1 * (1 - w) + y2[i] * w); 12 | } 13 | return y1 * (1 - w) + y2 * w; 14 | } 15 | 16 | // Sampling at at targetXs with linear interpolation 17 | // xs and ys must have the same length. 18 | //export 19 | function sampleAt(xs, ys, targetXs) { 20 | const descOrder = xs[0] > xs[1]; 21 | return targetXs.map((tx) => { 22 | let index = xs.findIndex((x) => (descOrder ? x <= tx : x >= tx)); 23 | if (index == -1) { 24 | index = xs.length - 1; 25 | } else if (index == 0) { 26 | index = 1; 27 | } 28 | return linearInterpolate(xs[index - 1], ys[index - 1], xs[index], ys[index], tx); 29 | }); 30 | } 31 | 32 | // x?s must be sorted in ascending order. 33 | // x?s and y?s must have the same length. 34 | // return [x, y] or null when no intersection found. 35 | //export 36 | function firstIntersection(x1s, y1s, x2s, y2s) { 37 | // Find all the points in the intersection of the 2 x ranges 38 | const min = Math.max(x1s[0], x2s[0]); 39 | const max = Math.min(x1s[x1s.length - 1], x2s[x2s.length - 1]); 40 | const xs = Array.from(new Set([...x1s, ...x2s])) 41 | .filter((x) => x >= min && x <= max) 42 | .sort((a, b) => (Number(a) > Number(b) ? 1 : -1)); 43 | // Interpolate the lines for all the points of that intersection 44 | const iy1s = sampleAt(x1s, y1s, xs); 45 | const iy2s = sampleAt(x2s, y2s, xs); 46 | // Check if each segment intersect 47 | for (let index = 0; index < xs.length - 1; index++) { 48 | const y11 = iy1s[index]; 49 | const y21 = iy2s[index]; 50 | const x1 = xs[index]; 51 | if (y11 == y21) { 52 | return [x1, y11]; 53 | } 54 | const y12 = iy1s[index + 1]; 55 | const y22 = iy2s[index + 1]; 56 | if (Math.sign(y21 - y11) != Math.sign(y22 - y12)) { 57 | const x2 = xs[index + 1]; 58 | const width = x2 - x1; 59 | const slope1 = (y12 - y11) / width; 60 | const slope2 = (y22 - y21) / width; 61 | const dx = (y21 - y11) / (slope1 - slope2); 62 | const dy = dx * slope1; 63 | return [x1 + dx, y11 + dy]; 64 | } 65 | } 66 | return null; 67 | } 68 | 69 | //export 70 | function zip(a, b) { 71 | return a.map((v, i) => [v, b[i]]); 72 | } 73 | 74 | //export 75 | function scaleLinear(from, to) { 76 | const scale = (v) => sampleAt(from, to, [v])[0]; 77 | scale.invert = (v) => sampleAt(to, from, [v])[0]; 78 | return scale; 79 | } 80 | 81 | //export 82 | function scaleLog(from, to) { 83 | from = from.map(Math.log); 84 | const scale = (v) => sampleAt(from, to, [Math.log(v)])[0]; 85 | scale.invert = (v) => Math.exp(sampleAt(to, from, [v])[0]); 86 | return scale; 87 | } 88 | 89 | //export 90 | function line(x, y) { 91 | return (d) => { 92 | const points = d.map((v) => x(v).toFixed(1) + "," + y(v).toFixed(1)); 93 | return "M" + points.join("L"); 94 | }; 95 | } 96 | 97 | //export 98 | function lerp(v0, v1, weight) { 99 | return v0 + weight * (v1 - v0); 100 | } 101 | 102 | export default { 103 | linearInterpolate, 104 | sampleAt, 105 | zip, 106 | firstIntersection, 107 | scaleLinear, 108 | scaleLog, 109 | line, 110 | lerp, 111 | } 112 | -------------------------------------------------------------------------------- /src/skewt.less: -------------------------------------------------------------------------------- 1 | .skew-t { 2 | 3 | position: relative; 4 | padding: 0px; 5 | 6 | .fnt { 7 | transition: font 0.3s; 8 | font: 10px Arial; 9 | font-family: -apple-system,system-ui,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif; 10 | } 11 | 12 | .mainsvg { 13 | background-color: transparent; 14 | } 15 | 16 | .controls, 17 | .range-container { 18 | .buttons { 19 | flex-grow: 1; 20 | margin: 3px; 21 | padding: 3px 0px 3.2px 0px; 22 | border-radius: 10px; 23 | text-align: center; 24 | //.fnt; 25 | cursor: pointer; 26 | line-height: 1.1em; 27 | background-color: rgba(220, 220, 220, 1); 28 | 29 | &.clicked { 30 | background-color: rgba(150, 150, 150, 1); 31 | color: white; 32 | } 33 | 34 | &.units { 35 | flex-grow: 0; 36 | width: 80px; 37 | } 38 | 39 | &.noclick { 40 | cursor: initial; 41 | } 42 | 43 | } 44 | margin-top: 10px; 45 | 46 | .row { 47 | display: flex; 48 | flex-wrap: wrap; 49 | } 50 | } 51 | 52 | .controls { 53 | box-sizing: border-box; 54 | width: 100%; 55 | display: flex; 56 | .values { 57 | flex-grow: 1; 58 | margin: 3px; 59 | padding: 3px; 60 | text-align: center; 61 | //.fnt; 62 | border-radius: 10px; 63 | border: 1px solid rgba(220, 220, 220, 1); 64 | min-width: 40px; 65 | } 66 | } 67 | 68 | .skewt-range-des { 69 | width: 20%; 70 | } 71 | 72 | .skewt-range-val { 73 | width: 15%; 74 | white-space: nowrap; 75 | } 76 | 77 | .checkbox-container { 78 | width: 100%; 79 | line-height: 20px; 80 | } 81 | 82 | .select-units{ 83 | :first-child{ 84 | line-height:20px; 85 | } 86 | } 87 | 88 | .axis path, 89 | .axis line { 90 | fill: none; 91 | stroke: #000; 92 | stroke-width: 1px; 93 | shape-rendering: crispEdges; 94 | } 95 | 96 | .x.axis path {} 97 | 98 | .y.axis path {} 99 | 100 | .axis { 101 | fill: #000; 102 | } 103 | 104 | .y.axis { 105 | font-size: 10px; 106 | } 107 | 108 | .y.axis.hght { 109 | font-size: 10px; 110 | fill: red; 111 | } 112 | 113 | .x.axis { 114 | font-size: 10px; 115 | } 116 | 117 | .y.axis.ticks text { 118 | display: none; 119 | } 120 | 121 | .y.axis.hght-ticks { 122 | text { 123 | display: none; 124 | } 125 | 126 | line { 127 | stroke: red; 128 | 129 | } 130 | 131 | } 132 | 133 | .skewt-line { 134 | fill: none; 135 | stroke-width: 1.5px; 136 | stroke-opacity: 0.5; 137 | 138 | &.highlight-line { 139 | stroke-opacity: 1; 140 | stroke-width: 2px; 141 | } 142 | } 143 | 144 | .temp { 145 | .skewt-line; 146 | stroke: red; 147 | 148 | } 149 | 150 | .dwpt { 151 | .skewt-line; 152 | stroke: blue; 153 | } 154 | 155 | .parcel { 156 | .skewt-line; 157 | stroke: green; 158 | stroke-opacity: 0.3; 159 | } 160 | 161 | .cond-level { 162 | .skewt-line; 163 | stroke-width: 1px; 164 | stroke: rgba(128, 128, 128, 0.8); 165 | stroke-opacity: 0.15; 166 | 167 | &.highlight-line { 168 | stroke-width: 1px; 169 | } 170 | } 171 | 172 | //.skline { stroke-width: 1.8px; } 173 | //.mean { stroke-width: 2.5px; } 174 | 175 | .gridline { 176 | stroke-width: 0.5px; 177 | stroke-opacity: 0.3; 178 | fill: none; 179 | 180 | &.highlight-line { 181 | stroke-opacity: 1; 182 | stroke-width: 1px; 183 | } 184 | } 185 | 186 | .tempzero { 187 | .gridline; 188 | stroke: #aaa; 189 | stroke-width: 1.25px; 190 | } 191 | 192 | .dryadiabat { 193 | .gridline; 194 | stroke: green; 195 | } 196 | 197 | .templine { 198 | .gridline; 199 | stroke: red; 200 | } 201 | 202 | .pressure { 203 | .gridline; 204 | stroke: rgba(120, 120, 120, 1); 205 | } 206 | 207 | .moistadiabat { 208 | .gridline; 209 | stroke: green; 210 | stroke-dasharray: 5; 211 | } 212 | 213 | .isohume { 214 | .gridline; 215 | stroke: blue; 216 | stroke-dasharray: 2; 217 | } 218 | 219 | .elr { 220 | .gridline; 221 | stroke: purple; 222 | stroke-opacity: 0.03; 223 | stroke-width: 3px; 224 | 225 | &.highlight-line { 226 | stroke-opacity: 0.7; 227 | } 228 | 229 | 230 | } 231 | 232 | .sigline { 233 | .gridline; 234 | 235 | &.surface { 236 | stroke: green; 237 | } 238 | 239 | &.tropopause-level { 240 | stroke: blue; 241 | } 242 | 243 | 244 | } 245 | 246 | .windbarb { 247 | stroke: #000; 248 | stroke-width: 0.75px; 249 | fill: none; 250 | //stroke-opacity: 1; 251 | 252 | .barblines { 253 | opacity: 0.4; 254 | &.highlight-line { 255 | opacity: 1; 256 | } 257 | &.hidden { 258 | display:none; 259 | } 260 | } 261 | 262 | .windtext{ 263 | opacity: 0.4; 264 | &.highlight-line { 265 | opacity: 1; 266 | } 267 | dominant-baseline:central; 268 | font-size:10px; 269 | fill:black; 270 | stroke-width:0; 271 | &.hidden { 272 | display:none; 273 | } 274 | } 275 | } 276 | 277 | 278 | .flag { 279 | fill: #000; 280 | } 281 | 282 | .overlay { 283 | fill: none; 284 | pointer-events: all; 285 | 286 | } 287 | 288 | .focus.tmpc circle { 289 | fill: red; 290 | stroke: none; 291 | } 292 | 293 | .focus.dwpc circle { 294 | fill: blue; 295 | stroke: none; 296 | } 297 | 298 | .focus text { 299 | font-size: 14px; 300 | } 301 | 302 | .skewt-wind-arrow { 303 | alignment-baseline: middle; 304 | text-anchor: middle; 305 | fill: black; 306 | font-size: 16px; 307 | font-weight: bold; 308 | } 309 | 310 | 311 | .range-container-extra { 312 | .range-container; 313 | margin-top: 0px; 314 | } 315 | 316 | .skewt-ranges { 317 | all: revert; 318 | -webkit-appearance: none; 319 | color: white; 320 | background-color: transparent;; 321 | width: 60%; 322 | 323 | &:focus { 324 | outline: none; 325 | } 326 | 327 | &::-webkit-slider-runnable-track { 328 | all: revert; 329 | -webkit-appearance: none; 330 | background: rgba(220, 220, 220, 1); 331 | border-radius: 16px; 332 | height: 16px; 333 | padding: 2px; 334 | 335 | } 336 | 337 | &::-webkit-slider-thumb { 338 | all: revert; 339 | -webkit-appearance: none; 340 | border: 1px solid rgba(100, 100, 100, 1); 341 | height: 12px; 342 | width: 12px; 343 | border-radius: 12px; 344 | background: #ffffff; 345 | cursor: pointer; 346 | //margin: 2px; /* You need to specify a margin in Chrome, but in Firefox and IE it is automatic */ 347 | box-shadow: 1px 1px 1px #000000; 348 | } 349 | } 350 | 351 | 352 | .flex-break { 353 | flex-basis: 100%; 354 | } 355 | 356 | .cloud-container { 357 | overflow: hidden; 358 | position: absolute; 359 | width: 20px; 360 | opacity: 0.8; 361 | 362 | .cloud { 363 | position: absolute; 364 | width: 10px; 365 | } 366 | } 367 | } -------------------------------------------------------------------------------- /src/skewt.mjs: -------------------------------------------------------------------------------- 1 | 2 | import atm from './atmosphere.mjs'; 3 | import clouds from './clouds.mjs'; 4 | import d3 from './d3.custom.min.mjs'; 5 | 6 | ////Original code from: 7 | 8 | /** 9 | * SkewT v1.1.0 10 | * 2016 David Félix - dfelix@live.com.pt 11 | * 12 | * Dependency: 13 | * d3.v3.min.js from https://d3js.org/ 14 | * 15 | */ 16 | 17 | 18 | window.SkewT= 19 | 20 | 21 | function (div, { isTouchDevice, gradient = 45, topp = 50, maxtopp=50, parctempShift = 2, height , margins = {}} = {}) { 22 | 23 | 24 | const _this = this; 25 | //properties used in calculations 26 | const outerWrapper = d3.select(div);//.style("overflow","hidden"); 27 | let width = parseInt(outerWrapper.style('width'), 10); 28 | const margin = { top: margins.top||10, right: margins.right||25, bottom: margins.bottom||10, left:margins.left || 25 }; //container margins 29 | const deg2rad = (Math.PI / 180); 30 | //var gradient = 46; 31 | 32 | let parctemp; //parctemp is only used to receive values with setParams. 33 | this.refs = {}; 34 | let adjustGradient = false; 35 | let tan; 36 | let basep = 1050; 37 | //var topp = 50; 38 | let pIncrement = -50; 39 | let midtemp = 0, temprange = 60, init_temprange = 60; 40 | let xOffset = 0; 41 | let xAxisTicks=40; 42 | let steph; //= atm.getElevation(topp) / 30; 43 | let moving = false; 44 | const K0 = 273.15; //Kelvin of 0 deg 45 | let selectedSkewt; 46 | let currentY = null;//used to store y position of tooltip, so filled at correct position of unit changed. 47 | 48 | const plines = [1000, 950, 925, 900, 850, 800, 700, 600, 500, 400, 300, 250, 200, 150, 100, 50]; 49 | 50 | const pticks = []; 51 | const tickInterval = 25; 52 | for (let i = plines[0] + tickInterval; i > plines[plines.length - 1]; i -= tickInterval) pticks.push(i); 53 | 54 | const altticks = []; 55 | for (let i = 0; i < 20000; i += (10000 / 3.28084)) altticks.push(atm.pressureFromElevation(i)); 56 | //console.log(altticks); 57 | 58 | const barbsize = 15; ///// 59 | // functions for Scales and axes. Note the inverted domain for the y-scale: bigger is up! 60 | const r = d3.scaleLinear().range([0, 300]).domain([0, 150]); 61 | const y2 = d3.scaleLinear(); 62 | const bisectTemp = d3.bisector(function (d) { return d.press; }).left; // bisector function for tooltips 63 | let w, h, x, y, xAxis, yAxis, yAxis2, yAxis3; 64 | let ymax; //log scale for max top pressure 65 | 66 | let dataSel = [], dataReversed = []; 67 | let dataAr = []; 68 | //aux 69 | const unitSpd = "kt"; // or kmh 70 | let unitAlt = "m"; 71 | let windDisplay = "Barbs"; 72 | 73 | if (isTouchDevice === void 0) { 74 | if (L && L.version) { //check if leaflet is loaded globally 75 | if (L.Browser.mobile) isTouchDevice = true; 76 | } else { 77 | isTouchDevice = ('ontouchstart' in window) || 78 | (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0); 79 | } 80 | } 81 | //console.log("this is a touch device:", isTouchDevice); 82 | 83 | 84 | 85 | //containers 86 | const wrapper = outerWrapper.append("div").style("position","relative"); 87 | const cloudContainer = wrapper.append("div").attr("class", "cloud-container"); 88 | const svg = wrapper.append("svg").attr("class", "mainsvg"); //main svg 89 | const controls = wrapper.append("div").attr("class", "controls fnt controls1"); 90 | const valuesContainer = wrapper.append("div").attr("class", "controls fnt controls2"); 91 | const rangeContainer = wrapper.append("div").attr("class", "range-container fnt"); 92 | const rangeContainer2 = wrapper.append("div").attr("class", "range-container-extra fnt"); 93 | const container = svg.append("g");//.attr("id", "container"); //container 94 | const skewtbg = container.append("g").attr("class", "skewtbg");//.attr("id", "skewtbg");//background 95 | const skewtgroup = container.append("g").attr("class", "skewt"); // put skewt lines in this group (class skewt not used) 96 | const barbgroup = container.append("g").attr("class", "windbarb"); // put barbs in this group 97 | const tooltipgroup = container.append("g").attr("class", "tooltips"); //class tooltps not used 98 | const tooltipRect = container.append("rect").attr("class", "overlay");//.attr("id", "tooltipRect") 99 | const cloudCanvas1 = cloudContainer.append("canvas").attr("width", 1).attr("height", 200).attr("class", "cloud"); //original = width 10 and height 300 100 | this.cloudRef1 = cloudCanvas1.node(); 101 | const cloudCanvas2 = cloudContainer.append("canvas").attr("width", 1).attr("height", 200).attr("class", "cloud"); 102 | this.cloudRef2 = cloudCanvas2.node(); 103 | 104 | 105 | function getFlags(f) { 106 | const flags = { 107 | "131072": "surface", 108 | "65536": "standard level", 109 | "32768": "tropopause level", 110 | "16384": "maximum wind level", 111 | "8192": "significant temperature level", 112 | "4096": "significant humidity level", 113 | "2048": "significant wind level", 114 | "1024": "beginning of missing temperature data", 115 | "512": "end of missing temperature data", 116 | "256": "beginning of missing humidity data", 117 | "128": "end of missing humidity data", 118 | "64": "beginning of missing wind data", 119 | "32": "end of missing wind data", 120 | "16": "top of wind sounding", 121 | "8": "level determined by regional decision", 122 | "4": "reserved", 123 | "2": "pressure level vertical coordinate" 124 | }; 125 | 126 | const foundflags = []; 127 | const decode = (a, i) => { 128 | if (a % 2) foundflags.push(flags[1 << i]); 129 | if (a) decode(a >> 1, i + 1); 130 | } 131 | decode(f, 0); 132 | //console.log(foundflags); 133 | return foundflags; 134 | } 135 | 136 | 137 | 138 | //local functions 139 | function setVariables() { 140 | width = parseInt(wrapper.style('width'), 10); 141 | height = height || width; 142 | //if (height>width) height = width; 143 | w = width - margin.left - margin.right; 144 | h = height - margin.top - margin.bottom; 145 | tan = Math.tan((gradient || 55) * deg2rad); 146 | //use the h for the x range, so that appearance does not change when resizing, remains square 147 | 148 | ymax = d3.scaleLog().range([0 ,h ]).domain([maxtopp, basep]); 149 | y = d3.scaleLog().range([0 ,h ]).domain([topp, basep]); 150 | 151 | temprange = init_temprange * (h-ymax(topp))/ (h-ymax(maxtopp)); 152 | x = d3.scaleLinear().range([w/2 - h*2, w/2 + h*2]).domain([midtemp - temprange * 4, midtemp + temprange * 4]); //range is w*2 153 | 154 | xAxisTicks = temprange < 40 ? 30: 40; 155 | xAxis = d3.axisBottom(x).tickSize(0, 0).ticks(xAxisTicks);//.orient("bottom"); 156 | yAxis = d3.axisLeft(y).tickSize(0, 0).tickValues(plines.filter(p => (p % 100 == 0 || p == 50 || p == 150))).tickFormat(d3.format(".0d"));//.orient("left"); 157 | yAxis2 = d3.axisRight(y).tickSize(5, 0).tickValues(pticks);//.orient("right"); 158 | yAxis3 = d3.axisLeft(y).tickSize(2, 0).tickValues(altticks); 159 | 160 | steph = atm.getElevation(topp) / (h/12); 161 | 162 | } 163 | 164 | function convSpd(msvalue, unit) { 165 | switch (unit) { 166 | case "kt": 167 | return msvalue * 1.943844492; 168 | //return msvalue; //wind is provided as kt by michael's program 169 | break; 170 | case "kmh": 171 | return msvalue * 3.6; 172 | break; 173 | default: 174 | return msvalue; 175 | } 176 | } 177 | function convAlt(v, unit) { 178 | switch (unit) { 179 | case "m": 180 | return Math.round(v) + unit; 181 | //return msvalue; //wind is provided as kt by michael's program 182 | break; 183 | case "f": 184 | return Math.round(v * 3.28084) + "ft"; 185 | break; 186 | default: 187 | return v; 188 | } 189 | } 190 | 191 | //assigns d3 events 192 | d3.select(window).on('resize', resize); 193 | 194 | function resize() { 195 | skewtbg.selectAll("*").remove(); 196 | setVariables(); 197 | svg.attr("width", w + margin.right + margin.left).attr("height", h + margin.top + margin.bottom); 198 | container.attr("transform", "translate(" + margin.left + "," + (margin.top) + ")"); 199 | drawBackground(); 200 | dataAr.forEach(d => { 201 | plot(d.data, { add: true, select: false }); 202 | });//redraw each plot 203 | if (selectedSkewt) selectSkewt(selectedSkewt.data); 204 | shiftXAxis(); 205 | tooltipRect.attr("width", w).attr("height", h); 206 | 207 | cloudContainer.style("left", (margin.left + 2) + "px").style("top", margin.top + "px").style("height", h + "px"); 208 | const canTop = y(100); //top of canvas for pressure 100 209 | cloudCanvas1.style("left", "0px").style("top", canTop + "px").style("height", (h - canTop) + "px"); 210 | cloudCanvas2.style("left", "10px").style("top", canTop + "px").style("height", (h - canTop) + "px"); 211 | } 212 | 213 | const lines = {}; 214 | let clipper; 215 | let xAxisValues; 216 | //let tempLine, tempdewLine; now in object 217 | 218 | 219 | const drawBackground = function () { 220 | 221 | // Add clipping path 222 | clipper = skewtbg.append("clipPath") 223 | .attr("id", "clipper") 224 | .append("rect") 225 | .attr("x", 0) 226 | .attr("y", 0 ) 227 | .attr("width", w) 228 | .attr("height", h ); 229 | 230 | // Skewed temperature lines 231 | lines.temp = skewtbg.selectAll("templine") 232 | .data(d3.scaleLinear().domain([midtemp - temprange * 4, midtemp + temprange*4]).ticks(xAxisTicks)) 233 | .enter().append("line") 234 | .attr("x1", d => x(d) - 0.5 + (y(basep) - y(topp)) / tan) 235 | .attr("x2", d => x(d) - 0.5) 236 | .attr("y1", 0) 237 | .attr("y2", h) 238 | .attr("class", d => d == 0 ? `tempzero ${buttons["Temp"].hi ? "highlight-line" : ""}` : `templine ${buttons["Temp"].hi ? "highlight-line" : ""}`) 239 | .attr("clip-path", "url(#clipper)"); 240 | //.attr("transform", "translate(0," + h + ") skewX(-30)"); 241 | 242 | 243 | /* 244 | let topTempOffset = x.invert(h/tan + w/2); 245 | let elevDiff = (atm.getElevation(topp) - atm.getElevation(basep));// * 3.28084; 246 | let km11y = h*(11000 - atm.getElevation(basep)) / elevDiff; 247 | let tempOffset11 = x.invert(km11y/tan + w/2); 248 | 249 | console.log("top temp shift", tempOffset11, x.invert(km11y/tan) ) ;//(elevDiff/304.8)); //deg per 1000ft 250 | */ 251 | 252 | const pp = moving ? 253 | [basep, basep - (basep - topp) * 0.25, basep - (basep - topp) * 0.5, basep - (basep - topp) * 0.75, topp] 254 | : d3.range(basep, topp - 50, pIncrement); 255 | 256 | 257 | const pAt11km = atm.pressureFromElevation(11000); 258 | //console.log(pAt11km); 259 | 260 | const elrFx = d3.line() 261 | .curve(d3.curveLinear) 262 | .x(function (d, i) { 263 | const e = atm.getElevation2(d); 264 | const t = d > pAt11km ? 15 - atm.getElevation(d) * 0.00649 : -56.5 //6.49 deg per 1000 m 265 | return x(t) + (y(basep) - y(d)) / tan; 266 | }) 267 | .y(function (d, i) { return y(d) }); 268 | 269 | lines.elr = skewtbg.selectAll("elr") 270 | .data([plines.filter(p => p > pAt11km).concat([pAt11km, 50])]) 271 | .enter().append("path") 272 | .attr("d", elrFx) 273 | .attr("clip-path", "url(#clipper)") 274 | .attr("class", `elr ${showElr ? "highlight-line" : ""}`); 275 | 276 | // Logarithmic pressure lines 277 | lines.pressure = skewtbg.selectAll("pressureline") 278 | .data(plines) 279 | .enter().append("line") 280 | .attr("x1", - w) 281 | .attr("x2", 2 * w) 282 | .attr("y1", y) 283 | .attr("y2", y) 284 | .attr("clip-path", "url(#clipper)") 285 | .attr("class", `pressure ${buttons["Pressure"].hi ? "highlight-line" : ""}`); 286 | 287 | // create array to plot adiabats 288 | 289 | const dryad = d3.scaleLinear().domain([midtemp - temprange * 2, midtemp + temprange * 6]).ticks(xAxisTicks); 290 | 291 | const all = []; 292 | 293 | for (let i = 0; i < dryad.length; i++) { 294 | const z = []; 295 | for (let j = 0; j < pp.length; j++) { z.push(dryad[i]); } 296 | all.push(z); 297 | } 298 | 299 | 300 | const drylineFx = d3.line() 301 | .curve(d3.curveLinear) 302 | .x(function (d, i) { 303 | return x( 304 | atm.dryLapse(pp[i], K0 + d, basep) - K0 305 | ) + (y(basep) - y(pp[i])) / tan; 306 | }) 307 | .y(function (d, i) { return y(pp[i]) }); 308 | 309 | // Draw dry adiabats 310 | lines.dryadiabat = skewtbg.selectAll("dryadiabatline") 311 | .data(all) 312 | .enter().append("path") 313 | .attr("class", `dryadiabat ${buttons["Dry Adiabat"].hi ? "highlight-line" : ""}`) 314 | .attr("clip-path", "url(#clipper)") 315 | .attr("d", drylineFx); 316 | 317 | // moist adiabat fx 318 | let temp; 319 | const moistlineFx = d3.line() 320 | .curve(d3.curveLinear) 321 | .x(function (d, i) { 322 | temp = i == 0 ? K0 + d : ((temp + atm.moistGradientT(pp[i], temp) * (moving ? (topp - basep) / 4 : pIncrement))) 323 | return x(temp - K0) + (y(basep) - y(pp[i])) / tan; 324 | }) 325 | .y(function (d, i) { return y(pp[i]) }); 326 | 327 | // Draw moist adiabats 328 | lines.moistadiabat = skewtbg.selectAll("moistadiabatline") 329 | .data(all) 330 | .enter().append("path") 331 | .attr("class", `moistadiabat ${buttons["Moist Adiabat"].hi ? "highlight-line" : ""}`) 332 | .attr("clip-path", "url(#clipper)") 333 | .attr("d", moistlineFx); 334 | 335 | // isohume fx 336 | let mixingRatio; 337 | const isohumeFx = d3.line() 338 | .curve(d3.curveLinear) 339 | .x(function (d, i) { 340 | //console.log(d); 341 | if (i == 0) mixingRatio = atm.mixingRatio(atm.saturationVaporPressure(d + K0), pp[i]); 342 | temp = atm.dewpoint(atm.vaporPressure(pp[i], mixingRatio)); 343 | return x(temp - K0) + (y(basep) - y(pp[i])) / tan; 344 | }) 345 | .y(function (d, i) { return y(pp[i]) }); 346 | 347 | // Draw isohumes 348 | lines.isohume = skewtbg.selectAll("isohumeline") 349 | .data(all) 350 | .enter().append("path") 351 | .attr("class", `isohume ${buttons["Isohume"].hi ? "highlight-line" : ""}`) 352 | .attr("clip-path", "url(#clipper)") 353 | .attr("d", isohumeFx); 354 | 355 | // Line along right edge of plot 356 | skewtbg.append("line") 357 | .attr("x1", w - 0.5) 358 | .attr("x2", w - 0.5) 359 | .attr("y1", 0) 360 | .attr("y2", h) 361 | .attr("class", "gridline"); 362 | 363 | // Add axes 364 | xAxisValues = skewtbg.append("g").attr("class", "x axis").attr("transform", "translate(0," + (h - 0.5 ) + ")").call(xAxis).attr("clip-path", "url(#clipper)"); 365 | skewtbg.append("g").attr("class", "y axis").attr("transform", "translate(-0.5,0)").call(yAxis); 366 | skewtbg.append("g").attr("class", "y axis ticks").attr("transform", "translate(-0.5,0)").call(yAxis2); 367 | skewtbg.append("g").attr("class", "y axis hght-ticks").attr("transform", "translate(-0.5,0)").call(yAxis3); 368 | } 369 | 370 | const makeBarbTemplates = function () { 371 | const speeds = d3.range(5, 205, 5); 372 | const barbdef = container.append('defs') 373 | speeds.forEach(function (d) { 374 | const thisbarb = barbdef.append('g').attr('id', 'barb' + d); 375 | const flags = Math.floor(d / 50); 376 | const pennants = Math.floor((d - flags * 50) / 10); 377 | const halfpennants = Math.floor((d - flags * 50 - pennants * 10) / 5); 378 | let px = barbsize / 2; 379 | // Draw wind barb stems 380 | thisbarb.append("line").attr("x1", 0).attr("x2", 0).attr("y1", -barbsize / 2).attr("y2", barbsize / 2); 381 | // Draw wind barb flags and pennants for each stem 382 | for (var i = 0; i < flags; i++) { 383 | thisbarb.append("polyline") 384 | .attr("points", "0," + px + " -6," + (px) + " 0," + (px - 2)) 385 | .attr("class", "flag"); 386 | px -= 5; 387 | } 388 | // Draw pennants on each barb 389 | for (i = 0; i < pennants; i++) { 390 | thisbarb.append("line") 391 | .attr("x1", 0) 392 | .attr("x2", -6) 393 | .attr("y1", px) 394 | .attr("y2", px + 2) 395 | px -= 3; 396 | } 397 | // Draw half-pennants on each barb 398 | for (i = 0; i < halfpennants; i++) { 399 | thisbarb.append("line") 400 | .attr("x1", 0) 401 | .attr("x2", -3) 402 | .attr("y1", px) 403 | .attr("y2", px + 1) 404 | px -= 3; 405 | } 406 | }); 407 | } 408 | 409 | 410 | const shiftXAxis = function () { 411 | clipper.attr("x", -xOffset); 412 | xAxisValues.attr("transform", `translate(${xOffset}, ${h - 0.5} )`); 413 | for (const p in lines) { 414 | lines[p].attr("transform", `translate(${xOffset},0)`); 415 | }; 416 | dataAr.forEach(d => { 417 | for (const p in d.lines) { 418 | d.lines[p].attr("transform", `translate(${xOffset},0)`); 419 | } 420 | }) 421 | } 422 | 423 | 424 | const drawToolTips = function () { 425 | 426 | // Draw tooltips 427 | const tmpcfocus = tooltipgroup.append("g").attr("class", "focus tmpc"); 428 | tmpcfocus.append("circle").attr("r", 4); 429 | tmpcfocus.append("text").attr("x", 9).attr("dy", ".35em"); 430 | 431 | const dwpcfocus = tooltipgroup.append("g").attr("class", "focus dwpc"); 432 | dwpcfocus.append("circle").attr("r", 4); 433 | dwpcfocus.append("text").attr("x", -9).attr("text-anchor", "end").attr("dy", ".35em"); 434 | 435 | const hghtfocus = tooltipgroup.append("g").attr("class", "focus"); 436 | const hght1 = hghtfocus.append("text").attr("x", "0.8em").attr("text-anchor", "start").attr("dy", ".35em"); 437 | const hght2 = hghtfocus.append("text").attr("x", "0.8em").attr("text-anchor", "start").attr("dy", "-0.65em").style("fill", "blue"); 438 | 439 | const wspdfocus = tooltipgroup.append("g").attr("class", "focus windspeed"); 440 | const wspd1 = wspdfocus.append("text").attr("x", "0.8em").attr("text-anchor", "start").attr("dy", ".35em"); 441 | const wspd2 = wspdfocus.append("text").attr("x", "0.8em").attr("text-anchor", "start").attr("dy", "-0.65em").style("fill", "red"); 442 | const wspd3 = wspdfocus.append("text").attr("class", "skewt-wind-arrow").html("⇩"); 443 | const wspd4 = wspdfocus.append("text").attr("y", "1em").attr("text-anchor", "start").style("fill", "rgba(0,0,0,0.3)").style("font-size", "10px"); 444 | //console.log(wspdfocus) 445 | 446 | let startX = null; 447 | 448 | 449 | function start(e) { 450 | showTooltips(); 451 | move.call(tooltipRect.node()); 452 | startX = d3.mouse(this)[0] - xOffset; 453 | } 454 | 455 | function end(e) { 456 | startX = null; 457 | } 458 | 459 | const hideTooltips = () => { 460 | [tmpcfocus, dwpcfocus, hghtfocus, wspdfocus].forEach(e => e.style("display", "none")); 461 | currentY = null; 462 | } 463 | hideTooltips(); 464 | 465 | const showTooltips = () => { 466 | [tmpcfocus, dwpcfocus, hghtfocus, wspdfocus].forEach(e => e.style("display", null)); 467 | } 468 | 469 | const move2P = (y0) => { 470 | //console.log("mving to", y0); 471 | if (y0 || y0===0) showTooltips(); 472 | const i = bisectTemp(dataReversed, y0, 1, dataReversed.length - 1); 473 | const d0 = dataReversed[i - 1]; 474 | const d1 = dataReversed[i]; 475 | const d = y0 - d0.press > d1.press - y0 ? d1 : d0; 476 | currentY = y0; 477 | 478 | tmpcfocus.attr("transform", "translate(" + (xOffset + x(d.temp) + (y(basep) - y(d.press)) / tan) + "," + y(d.press) + ")"); 479 | dwpcfocus.attr("transform", "translate(" + (xOffset + x(d.dwpt) + (y(basep) - y(d.press)) / tan) + "," + y(d.press) + ")"); 480 | 481 | hghtfocus.attr("transform", "translate(0," + y(d.press) + ")"); 482 | hght1.html("   " + ((d.hght || d.hght===0) ? convAlt(d.hght, unitAlt):"") ); //hgt or hghtagl ??? 483 | hght2.html("   " + Math.round(d.dwpt) + "°C"); 484 | 485 | wspdfocus.attr("transform", "translate(" + (w - (windDisplay=="Barbs" ? 70:80)) + "," + y(d.press) + ")"); 486 | wspd1.html(isNaN(d.wspd) ? "" : (Math.round(convSpd(d.wspd, unitSpd) * 10) / 10 + unitSpd)); 487 | wspd2.html(Math.round(d.temp) + "°C"); 488 | wspd3.style("transform", `rotate(${d.wdir}deg)`); 489 | wspd4.html(d.flags ? getFlags(d.flags).map(f => `${f}`).join() : ""); 490 | //console.log( getFlags(d.flags).join("
")); 491 | 492 | if (pressCbfs) pressCbfs.forEach(cbf=>cbf(d.press)); 493 | } 494 | 495 | function move(e) { 496 | const newX = d3.mouse(this)[0]; 497 | if (startX !== null) { 498 | xOffset = -(startX - newX); 499 | shiftXAxis(); 500 | } 501 | const y0 = y.invert(d3.mouse(this)[1]); // get y value of mouse pointer in pressure space 502 | move2P(y0); 503 | } 504 | 505 | tooltipRect 506 | .attr("width", w) 507 | .attr("height", h); 508 | 509 | //.on("mouseover", start) 510 | //.on("mouseout", end) 511 | //.on("mousemove", move) 512 | if (!isTouchDevice) { 513 | 514 | tooltipRect.call(d3.drag().on("start", start).on("drag", move).on("end", end)); 515 | } else { 516 | tooltipRect 517 | //tooltipRect.node().addEventListener('touchstart',start, true) 518 | //tooltipRect.node().addEventListener('touchmove',move, true) 519 | //tooltipRect.node().addEventListener('touchend',end, true) 520 | .on('touchstart', start) 521 | .on('touchmove', move) 522 | .on('touchend', end); 523 | } 524 | 525 | Object.assign(this, { move2P, hideTooltips, showTooltips }); 526 | } 527 | 528 | 529 | 530 | const drawParcelTraj = function (dataObj) { 531 | 532 | const { data, parctemp } = dataObj; 533 | 534 | if (data[0].dwpt == undefined) return; 535 | 536 | const pt = atm.parcelTrajectory( 537 | { level: data.map(e => e.press), gh: data.map(e => e.hght), temp: data.map(e => e.temp + K0) }, 538 | moving ? 10 : xAxisTicks, 539 | parctemp + K0, 540 | data[0].press, 541 | data[0].dwpt + K0 542 | ) 543 | 544 | //draw lines 545 | const parctrajFx = d3.line() 546 | .curve(d3.curveLinear) 547 | .x(function (d, i) { return x(d.t) + (y(basep) - y(d.p)) / tan; }) 548 | .y(function (d, i) { return y(d.p); }); 549 | 550 | //let parcLines={dry:[], moist:[], isohumeToDry:[], isohumeToTemp:[], moistFromCCL:[], TCONline:[], thrm:[], cloud:[]}; 551 | 552 | const parcLines = { parcel: [], LCL: [], CCL: [], TCON: [], "THRM top": [], "CLD top": [] }; 553 | 554 | for (const prop in parcLines) { 555 | const p = prop; 556 | if (dataObj.lines[p]) dataObj.lines[p].remove(); 557 | 558 | let line = [], press; 559 | switch (p) { 560 | case "parcel": 561 | if (pt.dry) line.push(pt.dry); 562 | if (pt.moist) line.push(pt.moist); 563 | break; 564 | case "TCON": 565 | const t = pt.TCON; 566 | line = t !== void 0 ? [[[t, basep], [t, topp]]] : []; 567 | break; 568 | case "LCL": 569 | if (pt.isohumeToDry) line.push(pt.isohumeToDry); 570 | break; 571 | case "CCL": 572 | if (pt.isohumeToTemp) line.push(pt.isohumeToTemp); 573 | if (pt.moistFromCCL) line.push(pt.moistFromCCL); 574 | break; 575 | case "THRM top": 576 | press = pt.pThermalTop; 577 | if (press) line = [[[0, press], [400, press]]]; 578 | break; 579 | case "CLD top": 580 | press = pt.pCloudTop; 581 | if (press) line = [[[0, press], [400, press]]]; 582 | break; 583 | } 584 | 585 | if (line) parcLines[p] = line.map(e => e.map(ee => { return { t: ee[0] - K0, p: ee[1] } })); 586 | 587 | dataObj.lines[p] = skewtgroup 588 | .selectAll(p) 589 | .data(parcLines[p]).enter().append("path") 590 | .attr("class", `${p == "parcel" ? "parcel" : "cond-level"} ${selectedSkewt && data == selectedSkewt.data && (p == "parcel" || values[p].hi) ? "highlight-line" : ""}`) 591 | .attr("clip-path", "url(#clipper)") 592 | .attr("d", parctrajFx) 593 | .attr("transform", `translate(${xOffset},0)`); 594 | } 595 | 596 | //update values 597 | for (const p in values) { 598 | let v = pt[p == "CLD top" ? "cloudTop" : p == "THRM top" ? "elevThermalTop" : p]; 599 | let CLDtopHi; 600 | if (p == "CLD top" && v == 100000) { 601 | v = data[data.length - 1].hght; 602 | CLDtopHi = true; 603 | } 604 | const txt = `${(p[0].toUpperCase() + p.slice(1)).replace(" ", " ")}:
${!v ? "" : p == "TCON" ? (v - K0).toFixed(1) + "°C" : (CLDtopHi ? "> " : "") + convAlt(v, unitAlt)}`; 605 | values[p].val.html(txt); 606 | } 607 | } 608 | 609 | const selectSkewt = function (data) { //use the data, then can be found from the outside by using data obj ref 610 | dataAr.forEach(d => { 611 | const found = d.data == data; 612 | for (const p in d.lines) { 613 | d.lines[p].classed("highlight-line", found && (!values[p] || values[p].hi)); 614 | } 615 | if (found) { 616 | selectedSkewt = d; 617 | dataReversed = [].concat(d.data).reverse(); 618 | ranges.parctemp.input.node().value = ranges.parctemp.value = d.parctemp = Math.round(d.parctemp * 10) / 10; 619 | ranges.parctemp.valueDiv.html(html4range(d.parctemp, "parctemp")); 620 | } 621 | }) 622 | _this.hideTooltips(); 623 | } 624 | 625 | 626 | 627 | //if in options: add, add new plot, 628 | //if select, set selected ix and highlight. if select false, must hightlight separtely. 629 | //ixShift used to shift to the right, used when you want to keep position 0 open. 630 | //max is the max number of plots, by default at the moment 2, 631 | const plot = function (s, { add, select, ixShift = 0, max = 2 } = {}) { 632 | 633 | if (s.length == 0) return; 634 | 635 | let ix = 0; //index of the plot, there may be more than one, to shift barbs and make clouds on canvas 636 | 637 | if (!add) { 638 | dataAr.forEach(d => { //clear all plots 639 | for (const p in d.lines) d.lines[p].remove(); 640 | }); 641 | dataAr = []; 642 | [1, 2].forEach(c => { 643 | const ctx = _this["cloudRef" + c].getContext("2d"); 644 | ctx.clearRect(0, 0, 10, 200); 645 | }); 646 | } 647 | 648 | let dataObj = dataAr.find(d => d.data == s); 649 | 650 | let data; 651 | 652 | if (!dataObj) { 653 | const parctemp = Math.round((s[0].temp + ranges.parctempShift.value) * 10) / 10; 654 | data = s; //do not filter here, filter creates new obj, looses ref 655 | //however, object itself can be changed. 656 | for(let i = 0; i=0 && data[i].rh<=100)){ 659 | let {rh, temp} = data[i]; 660 | data[i].dwpt = 243.04*(Math.log(rh/100)+((17.625*temp)/(243.04+temp)))/(17.625-Math.log(rh/100)-((17.625*temp)/(243.04+temp))) 661 | } 662 | } 663 | ix = dataAr.push({ data, parctemp, lines: {} }) - 1; 664 | dataObj = dataAr[ix]; 665 | if (ix >= max) { 666 | console.log("more than max plots added"); 667 | ix--; 668 | setTimeout((ix) => { 669 | if (dataAr.length > max) _this.removePlot(dataAr[ix].data); 670 | }, 1000, ix); 671 | } 672 | } else { 673 | ix = dataAr.indexOf(dataObj); 674 | data = dataObj.data; 675 | for (const p in dataObj.lines) dataObj.lines[p].remove(); 676 | } 677 | 678 | //reset parctemp range if this is the selected range 679 | if (select) { 680 | ranges.parctemp.input.node().value = ranges.parctemp.value = dataObj.parctemp; 681 | ranges.parctemp.valueDiv.html(html4range(dataObj.parctemp, "parctemp")); 682 | } 683 | 684 | //skew-t stuff 685 | 686 | // Filter data, depending on range moving, or nullish values 687 | 688 | let data4moving; 689 | if (data.length > 50 && moving) { 690 | let prev = -1; 691 | data4moving = data.filter((e, i, a) => { 692 | const n = Math.floor(i * 50 / (a.length - 1)); 693 | if (n > prev) { 694 | prev = n; 695 | return true; 696 | } 697 | }) 698 | } else { 699 | data4moving = data.map(e=>e); 700 | } 701 | let data4temp = [data4moving.filter(e=>( e.temp || e.temp===0 ) && e.temp>-999 )]; 702 | let data4dwpt = [data4moving.filter(e=>( e.dwpt || e.dwpt===0 ) && e.dwpt>-999 )]; 703 | 704 | 705 | 706 | 707 | 708 | const templineFx = d3.line().curve(d3.curveLinear).x(function (d, i) { return x(d.temp) + (y(basep) - y(d.press)) / tan; }).y(function (d, i) { return y(d.press); }); 709 | dataObj.lines.tempLine = skewtgroup 710 | .selectAll("templines") 711 | .data(data4temp).enter().append("path") 712 | .attr("class", "temp")//(d,i)=> `temp ${i<10?"skline":"mean"}` ) 713 | .attr("clip-path", "url(#clipper)") 714 | .attr("d", templineFx); 715 | 716 | const tempdewlineFx = d3.line().curve(d3.curveLinear).x(function (d, i) { return x(d.dwpt) + (y(basep) - y(d.press)) / tan; }).y(function (d, i) { return y(d.press); }); 717 | dataObj.lines.tempdewLine = skewtgroup 718 | .selectAll("tempdewlines") 719 | .data(data4dwpt).enter().append("path") 720 | .attr("class", "dwpt")//(d,i)=>`dwpt ${i<10?"skline":"mean"}` ) 721 | .attr("clip-path", "url(#clipper)") 722 | .attr("d", tempdewlineFx); 723 | 724 | drawParcelTraj(dataObj); 725 | 726 | 727 | 728 | const siglines = data 729 | .filter((d, i, a, f) => d.flags && (f = getFlags(d.flags), f.includes("tropopause level") || f.includes("surface")) ? d.press : false) 730 | .map((d, i, a, f) => (f = getFlags(d.flags), { press: d.press, classes: f.map(e => e.replace(/ /g, "-")).join(" ") })); 731 | 732 | dataObj.lines.siglines = skewtbg.selectAll("siglines") 733 | .data(siglines) 734 | .enter().append("line") 735 | .attr("x1", - w).attr("x2", 2 * w) 736 | .attr("y1", d => y(d.press)).attr("y2", d => y(d.press)) 737 | .attr("clip-path", "url(#clipper)") 738 | .attr("class", d => `sigline ${d.classes}`); 739 | 740 | 741 | //barbs stuff 742 | 743 | let lastH = -300; 744 | //filter barbs to be valid and not too crowded 745 | const barbs = data4moving.filter(function (d) { 746 | if (d.hght > lastH + steph && (d.wspd || d.wspd === 0) && d.press >= topp && !(d.wspd === 0 && d.wdir === 0)) lastH = d.hght; 747 | return d.hght == lastH; 748 | }); 749 | 750 | dataObj.lines.barbs = barbgroup.append("svg").attr("class", `barblines ${windDisplay=="Numerical"?"hidden":""}`);//.attr("transform","translate(30,80)"); 751 | dataObj.lines.barbs.selectAll("barbs") 752 | .data(barbs).enter().append("use") 753 | .attr("href", function (d) { return "#barb" + Math.round(convSpd(d.wspd, "kt") / 5) * 5; }) // 0,5,10,15,... always in kt 754 | .attr("transform", function (d) { return "translate(" + (w + 15 * (ix + ixShift)) + "," + y(d.press) + ") rotate(" + (d.wdir + 180) + ")"; }); 755 | 756 | 757 | dataObj.lines.windtext = barbgroup.append("svg").attr("class", `windtext ${windDisplay=="Barbs"?"hidden":""}`);//.attr("class", "barblines"); 758 | dataObj.lines.windtext.selectAll("windtext") 759 | .data(barbs).enter().append("g") 760 | .attr("transform",d=> `translate(${w + 28 * (ix + ixShift) - 20} , ${y(d.press)})`) 761 | dataObj.lines.windtext.selectAll("g").append("text") 762 | .html( "↑" ) 763 | .style("transform",d=> "rotate(" + (180 + d.wdir)+"deg)"); 764 | dataObj.lines.windtext.selectAll("g").append("text") 765 | .html( d=>Math.round(convSpd(d.wspd,"kt"))) 766 | .attr("x","0.5em"); 767 | 768 | ////clouds 769 | const clouddata = clouds.computeClouds(data); 770 | clouddata.canvas = _this["cloudRef" + (ix + ixShift + 1)]; 771 | clouds.cloudsToCanvas(clouddata); 772 | dataObj.cloudCanvas = clouddata.canvas; 773 | ////// 774 | 775 | if (select || dataAr.length == 1) { 776 | selectSkewt(dataObj.data); 777 | } 778 | shiftXAxis(); 779 | 780 | return dataAr.length; 781 | } 782 | 783 | 784 | //// controls at bottom 785 | 786 | var buttons = { "Dry Adiabat": {}, "Moist Adiabat": {}, "Isohume": {}, "Temp": {}, "Pressure": {} }; 787 | for (const p in buttons) { 788 | const b = buttons[p]; 789 | b.hi = false; 790 | b.el = controls.append("div").attr("class", "buttons").text(p).on("click", () => { 791 | b.hi = !b.hi; 792 | b.el.node().classList[b.hi ? "add" : "remove"]("clicked"); 793 | const line = p.replace(" ", "").toLowerCase(); 794 | lines[line]._groups[0].forEach(p => p.classList[b.hi ? "add" : "remove"]("highlight-line")); 795 | }) 796 | }; 797 | this.refs.highlightButtons = controls.node(); 798 | 799 | //values 800 | const values = { 801 | "surface": {}, 802 | "LCL": { hi: true }, 803 | "CCL": { hi: true }, 804 | "TCON": { hi: false }, 805 | "THRM top": { hi: false }, 806 | "CLD top": { hi: false } 807 | }; 808 | 809 | for (const prop in values) { 810 | const p = prop; 811 | const b = values[p]; 812 | b.val = valuesContainer.append("div").attr("class", `buttons ${p == "surface" ? "noclick" : ""} ${b.hi ? "clicked" : ""}`).html(p + ":"); 813 | if (/CCL|LCL|TCON|THRM top|CLD top/.test(p)) { 814 | b.val.on("click", () => { 815 | b.hi = !b.hi; 816 | b.val.node().classList[b.hi ? "add" : "remove"]("clicked"); 817 | selectedSkewt.lines[p]._groups[0].forEach(p => p.classList[b.hi ? "add" : "remove"]("highlight-line")); 818 | }) 819 | } 820 | } 821 | this.refs.valueButtons = valuesContainer.node(); 822 | 823 | const ranges = { 824 | parctemp: { value: 10, step: 0.1, min: -50, max: 50 }, 825 | topp: { min: 50, max: 900, step: 25, value: topp }, 826 | parctempShift: { min: -5, step: 0.1, max: 10, value: parctempShift }, 827 | gradient: { min: 0, max: 85, step: 1, value: gradient }, 828 | // midtemp:{value:0, step:2, min:-50, max:50}, 829 | 830 | }; 831 | 832 | const unit4range = p => p == "gradient" ? "°" : p == "topp" ? "hPa" : "°C"; 833 | 834 | const html4range = (v, p) => { 835 | let html = ""; 836 | if (p == "parctempShift" && r.value >= 0) html += "+"; 837 | html += (p == "gradient" || p == "topp" ? Math.round(v) : Math.round(v * 10) / 10) + unit4range(p); 838 | if (p == "parctemp") { 839 | const shift = selectedSkewt ? (Math.round((v - selectedSkewt.data[0].temp) * 10) / 10) : parctempShift; 840 | html += "  " + (shift > 0 ? "+" : "") + shift + ""; 841 | } 842 | return html; 843 | } 844 | 845 | for (const prop in ranges) { 846 | const p = prop; 847 | const contnr = p == "parctemp" || p == "topp" ? rangeContainer : rangeContainer2; 848 | const r = ranges[p]; 849 | r.row=contnr.append("div").attr("class","row");; 850 | this.refs[p]=r.row.node(); 851 | r.valueDiv = r.row.append("div").attr("class", "skewt-range-des").html(p == "gradient" ? "Gradient:" : p == "topp" ? "Top P:" : p == "parctemp" ? "Parcel T:" : "Parcel T Shift:"); 852 | r.valueDiv = r.row.append("div").attr("class", "skewt-range-val").html(html4range(r.value, p)); 853 | r.input = r.row.append("input").attr("type", "range").attr("min", r.min).attr("max", r.max).attr("step", r.step).attr("value", p == "gradient" ? 90 - r.value : r.value).attr("class", "skewt-ranges") 854 | .on("input", (a, b, c) => { 855 | 856 | _this.hideTooltips(); 857 | r.value = +c[0].value; 858 | 859 | if (p == "gradient") { 860 | gradient = r.value = 90 - r.value; 861 | showErlFor2Sec(0, 0, r.input); 862 | //console.log("GRADIENT ST", gradient); 863 | } 864 | if (p == "topp") { 865 | showErlFor2Sec(0, 0, r.input); 866 | const h_oldtopp = y(basep) - y(topp); 867 | topp = r.value; 868 | const h_newtopp = y(basep) - y(topp); 869 | pIncrement = topp > 500 ? -25 : -50; 870 | if (adjustGradient) { 871 | ranges.gradient.value = gradient = Math.atan(Math.tan(gradient * deg2rad) * h_oldtopp / h_newtopp) / deg2rad; 872 | ranges.gradient.input.node().value = 90 - gradient; //will trigger input event anyway 873 | ranges.gradient.valueDiv.html(html4range(gradient, "gradient")); 874 | init_temprange*=h_oldtopp/h_newtopp; 875 | if (ranges.gradient.cbfs) ranges.gradient.cbfs.forEach(cbf => cbf(gradient)); 876 | } else { 877 | // temprange = init_temprange * ph / pph; 878 | // setVariables(); 879 | } 880 | steph = atm.getElevation(topp) / 30; 881 | } 882 | if (p == "parctempShift") { 883 | parctempShift = r.value; 884 | } 885 | 886 | r.valueDiv.html(html4range(r.value, p)); 887 | 888 | clearTimeout(moving); 889 | moving = setTimeout(() => { 890 | moving = false; 891 | if (p == "parctemp") { 892 | if (selectedSkewt) drawParcelTraj(selectedSkewt); //value already set 893 | } else { 894 | resize(); 895 | } 896 | }, 1000) 897 | 898 | if (p == "parctemp"){ 899 | if (selectedSkewt) { 900 | selectedSkewt.parctemp = r.value; 901 | drawParcelTraj(selectedSkewt); 902 | } 903 | } else { 904 | resize(); 905 | } 906 | 907 | //this.cbfRange({ topp, gradient, parctempShift }); 908 | if (r.cbfs) r.cbfs.forEach(cbf => cbf(p=="gradient"? gradient: r.value)) 909 | }) 910 | 911 | //contnr.append("div").attr("class", "flex-break"); 912 | } 913 | 914 | 915 | let showElr; 916 | const showErlFor2Sec = (a, b, target) => { 917 | target = target[0] || target.node(); 918 | lines.elr.classed("highlight-line", true); 919 | clearTimeout(showElr); 920 | showElr = null; 921 | showElr = setTimeout(() => { 922 | target.blur(); 923 | lines.elr.classed("highlight-line", showElr = null); //background may be drawn again 924 | }, 1000); 925 | } 926 | 927 | ranges.gradient.input.on("focus", showErlFor2Sec); 928 | ranges.topp.input.on("focus", showErlFor2Sec); 929 | 930 | const cbSpan = rangeContainer2.append("span").attr("class", "row checkbox-container"); 931 | this.refs.maintainXCheckBox = cbSpan.node(); 932 | cbSpan.append("input").attr("type", "checkbox").on("click", (a, b, e) => { 933 | adjustGradient = e[0].checked; 934 | }); 935 | cbSpan.append("span").attr("class", "skewt-checkbox-text").html("Maintain temp range on X-axis when zooming"); 936 | 937 | const selectUnits = rangeContainer2.append("div").attr("class", "row select-units"); 938 | this.refs.selectUnits = selectUnits.node(); 939 | selectUnits.append("div").style("width","10em").html("Select alt units: "); 940 | const units = { "meter": {}, "feet": {} }; 941 | for (const prop in units) { 942 | const p = prop; 943 | units[p].hi = p[0] == unitAlt; 944 | units[p].el = selectUnits.append("div").attr("class", "buttons units" + (unitAlt == p[0] ? " clicked" : "")).text(p).on("click", () => { 945 | for (const p2 in units) { 946 | units[p2].hi = p == p2; 947 | units[p2].el.node().classList[units[p2].hi ? "add" : "remove"]("clicked"); 948 | } 949 | unitAlt = p[0]; 950 | if (currentY !== null) _this.move2P(currentY); 951 | drawParcelTraj(selectedSkewt); 952 | }) 953 | }; 954 | 955 | const selectWindDisp = rangeContainer2.append("div").attr("class", "row select-units"); 956 | this.refs.selectWindDisp = selectWindDisp.node(); 957 | selectWindDisp.append("div").style("width","10em").html("Select wind display: "); 958 | const windDisp = { "Barbs": {}, "Numerical": {} }; 959 | for (const prop in windDisp) { 960 | const p = prop; 961 | windDisp[p].hi = p == windDisplay; 962 | windDisp[p].el = selectWindDisp.append("div").attr("class", "buttons units" + (windDisplay == p ? " clicked" : "")).text(p).on("click", () => { 963 | for (const p2 in windDisp) { 964 | windDisp[p2].hi = p == p2; 965 | windDisp[p2].el.node().classList[windDisp[p2].hi ? "add" : "remove"]("clicked"); 966 | } 967 | windDisplay = p; 968 | //console.log(windDisplay); 969 | dataAr.forEach(d=>{ 970 | d.lines.barbs.classed("hidden", windDisplay=="Numerical"); 971 | d.lines.windtext.classed("hidden", windDisplay=="Barbs"); 972 | }) 973 | }) 974 | }; 975 | 976 | const removePlot = (s) => { //remove single plot 977 | const dataObj = dataAr.find(d => d.data == s); 978 | //console.log(dataObj); 979 | if (!dataObj) return; 980 | let ix=dataAr.indexOf(dataObj); 981 | //clear cloud canvas. 982 | if (dataObj.cloudCanvas){ 983 | const ctx = dataObj.cloudCanvas.getContext("2d"); 984 | ctx.clearRect(0, 0, 10, 200); 985 | } 986 | 987 | for (const p in dataObj.lines) { 988 | dataObj.lines[p].remove(); 989 | } 990 | dataAr.splice(ix, 1); 991 | if(dataAr.length==0) { 992 | _this.hideTooltips(); 993 | console.log("All plots removed"); 994 | } 995 | } 996 | 997 | const clear = () => { //remove all plots and data 998 | dataAr.forEach(d => { 999 | for (const p in d.lines) d.lines[p].remove(); 1000 | const ctx = d.cloudCanvas.getContext("2d"); 1001 | ctx.clearRect(0, 0, 10, 200); 1002 | }); 1003 | _this.hideTooltips(); 1004 | // these maybe not required, addressed by above. 1005 | skewtgroup.selectAll("lines").remove(); 1006 | skewtgroup.selectAll("path").remove(); //clear previous paths from skew 1007 | skewtgroup.selectAll("g").remove(); 1008 | barbgroup.selectAll("use").remove(); //clear previous paths from barbs 1009 | dataAr = []; 1010 | //if(tooltipRect)tooltipRect.remove(); tooltip rect is permanent 1011 | } 1012 | 1013 | const clearBg = () => { 1014 | skewtbg.selectAll("*").remove(); 1015 | } 1016 | 1017 | const setParams = (p) => { 1018 | ({ height=height, topp=topp, parctempShift=parctempShift, parctemp=parctemp, basep=basep, steph=steph, gradient=gradient } = p); 1019 | if (p=="gradient") ranges.gradient.input.value = 90 - p; 1020 | else if (ranges[p]) ranges[p].input.value = p; 1021 | //resize(); 1022 | } 1023 | 1024 | const getParams = () =>{ 1025 | return {height, topp, basep, steph, gradient, parctempShift, parctemp: selectSkewt.parctemp } 1026 | } 1027 | 1028 | const shiftDegrees = function (d) { 1029 | xOffset = x(0) - x(d) ; 1030 | //console.log("xOffs", xOffset); 1031 | shiftXAxis(); 1032 | } 1033 | 1034 | 1035 | // Event cbfs. 1036 | // possible events: temp, press, parctemp, topp, parctempShift, gradient; 1037 | 1038 | const pressCbfs=[]; 1039 | const tempCbfs=[]; 1040 | const on = (ev, cbf) =>{ 1041 | let evAr; 1042 | if (ev=="press" || ev=="temp") { 1043 | evAr=ev=="press"?pressCbfs:tempCbfs; 1044 | } else { 1045 | for (let p in ranges) { 1046 | if(ev.toLowerCase() == p.toLowerCase()){ 1047 | if (!ranges[p].cbfs) ranges[p].cbfs = []; 1048 | evAr=ranges[p].cbfs; 1049 | } 1050 | } 1051 | } 1052 | if (evAr){ 1053 | if (!evAr.includes(cbf)) { 1054 | evAr.push(cbf); 1055 | } else { 1056 | console.log("EVENT ALREADY REGISTERED"); 1057 | } 1058 | } else { 1059 | console.log("EVENT NOT RECOGNIZED"); 1060 | } 1061 | } 1062 | 1063 | const off = (ev, cbf) => { 1064 | let evAr; 1065 | if (ev=="press" || ev=="temp") { 1066 | evAr=ev=="press"?pressCbfs:tempCbfs; 1067 | } else { 1068 | for (let p in ranges) { 1069 | if(ranges[p].cbfs && ev.toLowerCase() == p.toLowerCase()){ 1070 | evAr=ranges[p].cbfs; 1071 | } 1072 | } 1073 | } 1074 | if (evAr) { 1075 | let ix = evAr.findIndex(c=>cbf==c); 1076 | if (ix>=0) evAr.splice(ix,1); 1077 | } 1078 | } 1079 | 1080 | // Add functions as public methods 1081 | 1082 | this.drawBackground = drawBackground; 1083 | this.resize = resize; 1084 | this.plot = plot; 1085 | this.clear = clear; //clear all the plots 1086 | this.clearBg = clearBg; 1087 | this.selectSkewt = selectSkewt; 1088 | this.removePlot = removePlot; //remove a specific plot, referenced by data object passed initially 1089 | 1090 | this.on = on; 1091 | this.off = off; 1092 | this.setParams = setParams; 1093 | this.getParams = getParams; 1094 | this.shiftDegrees = shiftDegrees; 1095 | 1096 | /** 1097 | * parcelTrajectory: 1098 | * @param params = {temp, gh, level}, 1099 | * @param {number} steps, 1100 | * @param surfacetemp, surf pressure and surf dewpoint 1101 | */ 1102 | this.parcelTrajectory = atm.parcelTrajectory; 1103 | 1104 | this.pressure2y = y; 1105 | this.temp2x = x; 1106 | this.gradient = gradient; //read only, use setParams to set. 1107 | 1108 | // this.move2P, this.hideTooltips, this.showTooltips, has been declared 1109 | 1110 | // this.cloudRef1 and this.cloudRef2 = references to the canvas elements to add clouds with other program 1111 | 1112 | this.refs.tooltipRect = tooltipRect.node(); 1113 | 1114 | /* other refs: 1115 | highlightButtons 1116 | valueButtons 1117 | parctemp 1118 | topp 1119 | gradient 1120 | parctempShift 1121 | maintainXCheckBox 1122 | selectUnits 1123 | selectWindDisp 1124 | tooltipRect 1125 | */ 1126 | 1127 | //init 1128 | setVariables(); 1129 | resize(); 1130 | drawToolTips.call(this); //only once 1131 | makeBarbTemplates(); //only once 1132 | }; 1133 | 1134 | -------------------------------------------------------------------------------- /windy_module/README.md: -------------------------------------------------------------------------------- 1 | # Skewt windy module 2 | 3 | In your plugin config.js, dependencies, add: 4 | 5 | `"https://unpkg.com/windyplugin-module-skewt"` 6 | 7 | This will register the skewt as an available plugin in Windy, but not load it yet. 8 | 9 | In your plugin: 10 | 11 | `const skewtMod = W.plugins.skewt;` 12 | 13 | then, when you need it: 14 | 15 | `skewtMod.open().then( `, Promise is just to show it is loaded, now you can start using it like this: 16 | 17 | `const mySkewt = new skewtMod.skewt(myDiv, {height:200, maxtopp: 50, topp:150, gradient: 50, margins:{top:0, left:25, right:15, bottom:0}});` 18 | 19 | See skewt README for functions. 20 | 21 | -------------------------------------------------------------------------------- /windy_module/skewtRegister.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //-- Register skewt as TagPlugin 4 | 5 | W.plugins["skewt"] = W.TagPlugin.instance({ 6 | ident: "skewt", 7 | hasURL: false, 8 | location: "https://unpkg.com/windyplugin-module-skewt/dist/skewt.js" 9 | }); 10 | 11 | --------------------------------------------------------------------------------