├── .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 |
--------------------------------------------------------------------------------