UNPKG

psychart

Version:

View air conditions on a psychrometric chart

535 lines (534 loc) 28.2 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Psychart = void 0; var viridis_1 = require("viridis"); var psystate_1 = require("./psystate"); var SMath = require("smath"); var defaults_1 = require("./defaults"); var chart_1 = require("../chart"); /** * Generates an interactive psychrometric chart with plotting capabilities. */ var Psychart = /** @class */ (function (_super) { __extends(Psychart, _super); /** * Construct a new instance of `Psychart` given various configuration properties. */ function Psychart(options) { if (options === void 0) { options = {}; } var _this = _super.call(this, options, defaults_1.defaultPsychartOptions) || this; /** * Defines the string representations of the current unit system. */ _this.units = { temp: '', hr: '', vp: '', h: '', v: '', }; /** * Represents the legend for Psychart. */ _this.legend = document.createElementNS(_this.NS, 'svg'); /** * Legend definitions, which contains linear gradients. */ _this.legendDefs = document.createElementNS(_this.NS, 'defs'); /** * The base element for adding lines in the legend. */ _this.legendg = document.createElementNS(_this.NS, 'g'); /** * Defines all the groups in the SVG ordered by layer. */ _this.g = { regions: document.createElementNS(_this.NS, 'g'), axes: document.createElementNS(_this.NS, 'g'), text: document.createElementNS(_this.NS, 'g'), trends: document.createElementNS(_this.NS, 'g'), points: document.createElementNS(_this.NS, 'g'), tooltips: document.createElementNS(_this.NS, 'g'), }; /** * The data series plotted on Psychart with each of their last states and visibility toggles. */ _this.series = {}; // Check to make sure that dpMax is less than dbMax if (_this.options.dpMax > _this.options.dbMax) { throw new Error('Dew point maximum is greater than dry bulb range!'); } // Set base styling. _this.base.style.position = 'relative'; // If set, generate the legend. if (typeof _this.options.legend === 'object') { // Set the legend's viewport size. _this.legend.setAttribute('viewBox', '0 0 ' + _this.options.size.x + ' ' + _this.getLegendHeight()); _this.legend.setAttribute('width', _this.options.size.x + 'px'); _this.legend.setAttribute('height', _this.getLegendHeight() + 'px'); _this.legend.appendChild(_this.legendDefs); _this.legend.appendChild(_this.createLabel(_this.options.legend.title, { x: 0, y: 0 }, viridis_1.Color.hex(_this.options.colors.font), 1 /* TextAnchor.NW */)); _this.legend.appendChild(_this.legendg); // Attach elements to the base element. var legendContainer = document.createElement('div'); legendContainer.setAttribute('title', 'Click to toggle data series visibility.'); legendContainer.style.position = 'absolute'; legendContainer.style.left = (_this.options.flipXY ? (_this.options.size.x - _this.options.legend.size.x - _this.options.legend.margin.x) : _this.options.legend.margin.x) + 'px'; legendContainer.style.top = (_this.options.flipXY ? (_this.options.size.y - _this.options.legend.size.y - _this.options.legend.margin.y) : _this.options.legend.margin.y) + 'px'; legendContainer.style.width = _this.options.legend.size.x + 'px'; legendContainer.style.height = _this.options.legend.size.y + 'px'; legendContainer.style.overflowX = 'hidden'; legendContainer.style.overflowY = 'auto'; legendContainer.style.border = '1px solid ' + _this.options.colors.axis; legendContainer.appendChild(_this.legend); _this.base.appendChild(legendContainer); } // Sets the displayed units based on the unit system. _this.units.temp = '\u00B0' + (_this.options.unitSystem === 'IP' ? 'F' : 'C'); _this.units.hr = (_this.options.unitSystem === 'IP' ? 'lbw/klba' : 'gw/kga'); _this.units.vp = (_this.options.unitSystem === 'IP' ? 'Psi' : 'Pa'); _this.units.h = (_this.options.unitSystem === 'IP' ? 'Btu/lb' : 'kJ/kg'); _this.units.v = (_this.options.unitSystem === 'IP' ? 'ft\u00B3/lb' : 'm\u00B3/kg'); // Set the scaling factors for different unit systems. _this.scaleFactor = { hr: (_this.options.unitSystem === 'IP' ? 1e3 : 1e3), h: (_this.options.unitSystem === 'IP' ? 1 : 1e-3), }; // Create new SVG groups, and append all the // layers into the chart. Object.values(_this.g).forEach(function (group) { return _this.svg.appendChild(group); }); // Draw constant dry bulb vertical lines. Psychart.getRange(_this.options.dbMin, _this.options.dbMax, _this.options.major.temp).forEach(function (db) { var data = []; // The lower point is on the X-axis (rh = 0%) data.push(new psystate_1.PsyState({ db: db, other: 0, measurement: 'dbrh' }, _this.options)); // The upper point is on the saturation line (rh = 100%) data.push(new psystate_1.PsyState({ db: db, other: 1, measurement: 'dbrh' }, _this.options)); // Draw the axis and the label _this.drawAxis(data); _this.drawLabel(db + (_this.options.showUnits.axis ? _this.units.temp : ''), data[0], 2 /* TextAnchor.N */, 'Dry Bulb' + (_this.options.showUnits.tooltip ? ' [' + _this.units.temp + ']' : '')); }); switch (_this.options.yAxis) { case ('dp'): { // Draw constant dew point horizontal lines. Psychart.getRange(0, _this.options.dpMax, _this.options.major.temp).forEach(function (dp) { var data = []; // The left point is on the saturation line (db = dp) data.push(new psystate_1.PsyState({ db: dp, other: dp, measurement: 'dbdp' }, _this.options)); // The right point is at the maximum dry bulb temperature data.push(new psystate_1.PsyState({ db: _this.options.dbMax, other: dp, measurement: 'dbdp' }, _this.options)); // Draw the axis and the label _this.drawAxis(data); _this.drawLabel(dp + (_this.options.showUnits.axis ? _this.units.temp : ''), data[1], 8 /* TextAnchor.W */, 'Dew Point' + (_this.options.showUnits.tooltip ? ' [' + _this.units.temp + ']' : '')); }); break; } case ('hr'): { // Draw constant humidity ratio horizontal lines. var maxHr = new psystate_1.PsyState({ db: _this.options.dbMax, measurement: 'dbdp', other: _this.options.dpMax }, _this.options).hr, step = _this.options.major.humRat / _this.scaleFactor.hr; Psychart.getRange(0, maxHr, step).forEach(function (hr) { var data = []; // The right point is at the maximum dry bulb temperature data.push(new psystate_1.PsyState({ db: _this.options.dbMax, other: hr, measurement: 'dbhr' }, _this.options)); // The left point is on the saturation line var dp = data[data.length - 1].dp; data.push(new psystate_1.PsyState({ db: dp, other: dp, measurement: 'dbdp' }, _this.options)); // Draw the axis and the label _this.drawAxis(data); _this.drawLabel(SMath.round2(hr * _this.scaleFactor.hr, 1) + (_this.options.showUnits.axis ? _this.units.hr : ''), data[0], 8 /* TextAnchor.W */, 'Humidity Ratio' + (_this.options.showUnits.tooltip ? ' [' + _this.units.hr + ']' : '')); }); break; } default: { throw new Error('Invalid y-axis type: ' + _this.options.yAxis); } } // Draw constant wet bulb diagonal lines. Psychart.getRange(_this.options.dbMin, _this.options.dpMax, _this.options.major.temp).forEach(function (wb) { var data = []; // Dry bulb is always equal or greater than wet bulb. for (var db = wb; db <= _this.options.dbMax; db += _this.options.resolution) { data.push(new psystate_1.PsyState({ db: db, other: wb, measurement: 'dbwb' }, _this.options)); } // Draw the axis and the label _this.drawAxis(data); _this.drawLabel(wb + (_this.options.showUnits.axis ? _this.units.temp : ''), data[0], 5 /* TextAnchor.SE */, 'Wet Bulb' + (_this.options.showUnits.tooltip ? ' [' + _this.units.temp + ']' : '')); }); // Draw constant relative humidity lines. Psychart.getRange(0, 100, _this.options.major.relHum).forEach(function (rh) { var data = []; var preferredAnchor = 3 /* TextAnchor.NE */; // Must iterate through all dry bulb temperatures to calculate each Y-coordinate for (var db = _this.options.dbMin; db <= _this.options.dbMax; db += _this.options.resolution) { data.push(new psystate_1.PsyState({ db: db, other: rh / 100, measurement: 'dbrh' }, _this.options)); // Stop drawing when the line surpasses the bounds of the chart if (data[data.length - 1].dp >= _this.options.dpMax) { preferredAnchor = 6 /* TextAnchor.S */; break; } } // Draw the axis and the label _this.drawAxis(data); if (rh > 0 && rh < 100) { _this.drawLabel(rh + (_this.options.showUnits.axis ? '%' : ''), data[data.length - 1], preferredAnchor, 'Relative Humidity' + (_this.options.showUnits.tooltip ? ' [%]' : '')); } }); // Draw any regions, if applicable var regionIndex = 0; Object.entries(defaults_1.regions) .filter(function (_a) { var _b; var name = _a[0]; return (_b = _this.options.regions) === null || _b === void 0 ? void 0 : _b.includes(name); }) .forEach(function (_a) { var region = _a[1]; // Force region gradient to remain within subrange of full span to improve visual impact in light/dark themes var minRegion = 0 + -1, // -1 (arbitrary) Affects minimum span of region maxRegion = _this.options.regions.length - 1 + 4, // +4 (arbitrary) Affects maximum span of region minSpan = (_this.options.flipGradients) ? maxRegion : minRegion, maxSpan = (_this.options.flipGradients) ? minRegion : maxRegion, data = chart_1.Chart.deepCopy(region.data); if (_this.options.unitSystem === 'IP') { // Convert from SI to US units data.forEach(function (datum) { datum.db = Psychart.CtoF(datum.db); if (datum.measurement === 'dbdp' || datum.measurement === 'dbwb') { datum.other = Psychart.CtoF(datum.other); } }); } _this.drawRegion(data, viridis_1.Palette[_this.options.colors.regionGradient].getColor(regionIndex, minSpan, maxSpan), region.tooltip); regionIndex++; }); return _this; } /** * Helper function to return an array of region names and their corresponding tooltips. */ Psychart.getRegionNamesAndTips = function () { return Object.entries(defaults_1.regions).map(function (_a) { var name = _a[0], region = _a[1]; return [name, region.tooltip]; }); }; /** * Convert from Celsius to Fahrenheit. */ Psychart.CtoF = function (C) { return SMath.translate(C, 0, 100, 32, 212); }; /** * Get a range of numbers used for an axis. */ Psychart.getRange = function (min, max, step) { var stepMin = SMath.round2(min + step * 1.1, step), stepMax = SMath.round2(max - step * 1.1, step), range = []; for (var i = stepMin; i <= stepMax; i += step) { range.push(i); } return __spreadArray(__spreadArray([min], range, true), [max], false); }; /** * Interpolate between "corner" psychrometric states. */ Psychart.prototype.interpolate = function (states, crop) { // Cannot interpolate with only 1 point! if (states.length < 2) { return states; } // Create array containing interpolated points, starting with the first state. var data = [states[0]]; for (var i = 1; i < states.length; i++) { // Determine the start and end states. var start = states[i - 1]; var end = states[i]; // Check if iso-rh, iso-wb, or iso-dp curved or straight lines if (start.state.measurement === end.state.measurement && SMath.approx(start.state.other, end.state.other)) { // Determine the dry bulb range var startDb = crop ? SMath.clamp(start.db, this.options.dbMin, this.options.dbMax) : start.db; var endDb = crop ? SMath.clamp(end.db, this.options.dbMin, this.options.dbMax) : end.db; var nPoints = Math.abs((startDb - endDb) / this.options.resolution) | 0; var dbRange = SMath.linspace(startDb, endDb, nPoints); // Compute several intermediate states with a step of `resolution` for (var _i = 0, dbRange_1 = dbRange; _i < dbRange_1.length; _i++) { var db = dbRange_1[_i]; data.push(new psystate_1.PsyState({ db: db, other: start.state.other, measurement: start.state.measurement }, this.options)); // Stop generating if dew point exceeds maximum if (crop && data[data.length - 1].dp > this.options.dpMax) { break; } } } else { data.push(end); } } return data; }; /** * Generate SVG path data from an array of psychrometric states. */ Psychart.prototype.setPathData = function (path, psystates, closePath) { path.setAttribute('d', 'M ' + psystates.map(function (psy) { var xy = psy.toXY(); return xy.x + ',' + xy.y; }).join(' ') + (closePath ? ' z' : '')); }; /** * Draw an axis line given an array of psychrometric states. */ Psychart.prototype.drawAxis = function (data) { this.g.axes.appendChild(this.createLine(data, viridis_1.Color.hex(this.options.colors.axis), 1.0)); }; /** * Create a line to append onto a parent element. */ Psychart.prototype.createLine = function (data, color, weight) { var line = document.createElementNS(this.NS, 'path'); line.setAttribute('fill', 'none'); line.setAttribute('stroke', color.toString()); line.setAttribute('stroke-width', weight + 'px'); line.setAttribute('stroke-linecap', 'round'); line.setAttribute('vector-effect', 'non-scaling-stroke'); // Convert the array of psychrometric states into an array of (x,y) points. this.setPathData(line, data, false); return line; }; /** * Draw an axis label. */ Psychart.prototype.drawLabel = function (text, location, anchor, tooltip) { var _this = this; // Determine if anchor needs to be mirrored if (this.options.flipXY) { switch (anchor) { case (4 /* TextAnchor.E */): { anchor = 2 /* TextAnchor.N */; break; } case (2 /* TextAnchor.N */): { anchor = 4 /* TextAnchor.E */; break; } case (1 /* TextAnchor.NW */): { anchor = 5 /* TextAnchor.SE */; break; } case (5 /* TextAnchor.SE */): { anchor = 1 /* TextAnchor.NW */; break; } case (6 /* TextAnchor.S */): { anchor = 8 /* TextAnchor.W */; break; } case (8 /* TextAnchor.W */): { anchor = 6 /* TextAnchor.S */; break; } } } var fontColor = viridis_1.Color.hex(this.options.colors.font), label = this.createLabel(text, location.toXY(), fontColor, anchor); this.g.text.appendChild(label); if (tooltip) { label.addEventListener('mouseover', function (e) { return _this.drawTooltip(tooltip, { x: e.offsetX, y: e.offsetY }, fontColor, _this.g.tooltips); }); label.addEventListener('mouseleave', function () { return chart_1.Chart.clearChildren(_this.g.tooltips); }); } }; /** * Add a line to the legend. */ Psychart.prototype.addToLegend = function (seriesName, color, gradient) { var _this = this; this.legend.setAttribute('viewBox', '0 0 ' + this.options.size.x + ' ' + this.getLegendHeight()); this.legend.setAttribute('height', this.getLegendHeight() + 'px'); var g = document.createElementNS(this.NS, 'g'), icon = document.createElementNS(this.NS, 'rect'); g.setAttribute('cursor', 'pointer'); icon.setAttribute('x', (this.options.font.size / 2).toString()); icon.setAttribute('y', (this.getLegendHeight() - this.options.font.size * 1.5).toString()); icon.setAttribute('width', this.options.font.size.toString()); icon.setAttribute('height', this.options.font.size.toString()); icon.setAttribute('rx', (this.options.font.size * 0.20).toString()); if (color) { icon.setAttribute('fill', color.toString()); } else if (gradient) { var uniqueGradientID = 'psy_' + this.id + '_grad_' + this.legendDefs.children.length; this.legendDefs.appendChild(viridis_1.Palette[gradient].toSVG(uniqueGradientID)); icon.setAttribute('fill', 'url(#' + uniqueGradientID + ')'); } else { throw new Error('Error in ' + seriesName + '. Must have color or gradient defined.'); } var fontColor = viridis_1.Color.hex(this.options.colors.font), legendText = this.createLabel(seriesName, { x: this.options.font.size * 1.5, y: this.getLegendHeight() - this.options.font.size }, fontColor, 8 /* TextAnchor.W */); g.addEventListener('click', function () { _this.series[seriesName].hidden = !_this.series[seriesName].hidden; if (_this.series[seriesName].hidden) { g.setAttribute('opacity', '0.5'); legendText.setAttribute('text-decoration', 'line-through'); // Remove elements of this series from Psychart _this.g.points.removeChild(_this.series[seriesName].pointGroup); _this.g.trends.removeChild(_this.series[seriesName].lineGroup); } else { g.removeAttribute('opacity'); legendText.removeAttribute('text-decoration'); // Re-add elements of this series to Psychart _this.g.points.appendChild(_this.series[seriesName].pointGroup); _this.g.trends.appendChild(_this.series[seriesName].lineGroup); } }); g.append(icon, legendText); this.legendg.appendChild(g); }; /** * Compute the height of the legend, in pixels. */ Psychart.prototype.getLegendHeight = function () { return (this.legendg.children.length + 2.5) * this.options.font.size * this.options.lineHeight; }; /** * Plot one psychrometric state onto the psychrometric chart. */ Psychart.prototype.plot = function (state, config) { var _this = this; if (config === void 0) { config = {}; } // Skip series that are missing a measurement point. if (!Number.isFinite(state.db) || !Number.isFinite(state.other)) { return; } // Set default data options. var options = chart_1.Chart.setDefaults(config, defaults_1.defaultDataOptions); // Determine whether this is time-dependent. var hasTimeStamp = Number.isFinite(options.time.now), timeSeries = hasTimeStamp && Number.isFinite(options.time.end) && Number.isFinite(options.time.start); // Divide by 100 if relHumType is set to 'percent' if (state.measurement === 'dbrh' && options.relHumType === 'percent') { state.other /= 100; } var currentState = new psystate_1.PsyState(state, this.options), location = currentState.toXY(); // Compute the current color to plot var tMin = (this.options.flipGradients) ? options.time.end : options.time.start, tMax = (this.options.flipGradients) ? options.time.start : options.time.end, tNow = options.time.now, color = timeSeries ? viridis_1.Palette[options.gradient].getColor(tNow, tMin, tMax) : viridis_1.Color.hex(options.color); // Define a 0-length path element and assign its attributes. var point = document.createElementNS(this.NS, 'path'); point.setAttribute('fill', 'none'); point.setAttribute('stroke', color.toString()); point.setAttribute('stroke-width', +options.pointRadius + 'px'); point.setAttribute('stroke-linecap', 'round'); point.setAttribute('vector-effect', 'non-scaling-stroke'); point.setAttribute('d', 'M ' + location.x + ',' + location.y + ' h 0'); // Determine whether to draw a line from another point. var lineFrom = null; // Options for data series: if (options.name) { // Add an item in the legend, if not previously added. if (!this.series[options.name]) { this.series[options.name] = { lastState: currentState, hidden: false, pointGroup: document.createElementNS(this.NS, 'g'), lineGroup: document.createElementNS(this.NS, 'g'), }; // Add the series-level group elements onto the main groups. this.g.points.appendChild(this.series[options.name].pointGroup); this.g.trends.appendChild(this.series[options.name].lineGroup); if (options.legend) { this.addToLegend(options.name, timeSeries ? undefined : color, timeSeries ? options.gradient : undefined); } } else if (options.line === true) { // Determine whether to connect the states with a line lineFrom = this.series[options.name].lastState; } // Store the last state in order to draw a line. this.series[options.name].lastState = currentState; // Plot the new data point onto the series group element. this.series[options.name].pointGroup.appendChild(point); } else { // Plot the new data point onto the base group element. this.g.points.appendChild(point); } // Check for arbitrary origin point to draw a line. if (typeof options.line === 'object') { lineFrom = new psystate_1.PsyState(options.line, this.options); } // Draw a line. if (lineFrom) { var line = this.createLine(this.interpolate([lineFrom, currentState], true), color, options.pointRadius / 2); if (options.name) { this.series[options.name].lineGroup.appendChild(line); } else { this.g.trends.appendChild(line); } } // Generate the text to display on mouse hover. var tooltipString = (options.name ? options.name + '\n' : '') + (hasTimeStamp ? new Date(tNow).toLocaleString() + '\n' : '') + currentState.db.toFixed(1) + this.units.temp + ' Dry Bulb\n' + (currentState.rh * 100).toFixed() + '% Rel. Hum.\n' + currentState.wb.toFixed(1) + this.units.temp + ' Wet Bulb\n' + currentState.dp.toFixed(1) + this.units.temp + ' Dew Point' + (options.advanced ? '\n' + (currentState.hr * this.scaleFactor.hr).toFixed(2) + ' ' + this.units.hr + ' Hum. Ratio\n' + currentState.vp.toFixed(1) + ' ' + this.units.vp + ' Vap. Press.\n' + (currentState.h * this.scaleFactor.h).toFixed(1) + ' ' + this.units.h + ' Enthalpy\n' + currentState.v.toFixed(2) + ' ' + this.units.v + ' Volume\n' + (currentState.s * 100).toFixed() + '% Saturation' : ''); // Set the behavior when the user interacts with this point point.addEventListener('mouseover', function (e) { return _this.drawTooltip(tooltipString, { x: e.offsetX, y: e.offsetY }, color, _this.g.tooltips); }); point.addEventListener('mouseleave', function () { return chart_1.Chart.clearChildren(_this.g.tooltips); }); }; /** * Draw a shaded region on Psychart. */ Psychart.prototype.drawRegion = function (data, color, tooltip) { var _this = this; // Interpolate to get a set of psychrometric states that make the border of the region var states = this.interpolate(data.map(function (datum) { return new psystate_1.PsyState(datum, _this.options); }), false); // Create the SVG element to render the shaded region var region = document.createElementNS(this.NS, 'path'); region.setAttribute('fill', color.toString()); this.setPathData(region, states, true); this.g.regions.appendChild(region); // Optionally render a tooltip on mouse hover if (tooltip) { region.addEventListener('mouseover', function (e) { return _this.drawTooltip(tooltip, { x: e.offsetX, y: e.offsetY }, color, _this.g.tooltips); }); region.addEventListener('mouseleave', function () { return chart_1.Chart.clearChildren(_this.g.tooltips); }); } }; /** * Clear all plotted data from Psychart. */ Psychart.prototype.clearData = function () { this.series = {}; chart_1.Chart.clearChildren(this.g.points); chart_1.Chart.clearChildren(this.g.trends); chart_1.Chart.clearChildren(this.legendDefs); chart_1.Chart.clearChildren(this.legendg); }; return Psychart; }(chart_1.Chart)); exports.Psychart = Psychart;