psychart
Version:
View air conditions on a psychrometric chart
377 lines (376 loc) • 18.4 kB
JavaScript
"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 __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Pumpchart = void 0;
var SMath = require("smath");
var chart_1 = require("../chart");
var defaults_1 = require("./defaults");
var viridis_1 = require("viridis");
var lib_1 = require("./lib");
var units_1 = require("./units");
var dimensional_1 = require("dimensional");
/**
* Show a pump's relationship between flow rate and pressure at different operating conditions.
*/
var Pumpchart = /** @class */ (function (_super) {
__extends(Pumpchart, _super);
/**
* Create a new Pumpchart with custom options.
* @param options Customization options for the new Pumpchart.
*/
function Pumpchart(options) {
if (options === void 0) { options = {}; }
var _this = _super.call(this, options, defaults_1.defaultOptions) || this;
/**
* Layers of the SVG as groups.
*/
_this.g = {
hilites: document.createElementNS(_this.NS, 'g'),
curves: document.createElementNS(_this.NS, 'g'),
axes: document.createElementNS(_this.NS, 'g'),
data: document.createElementNS(_this.NS, 'g'),
text: document.createElementNS(_this.NS, 'g'),
tips: document.createElementNS(_this.NS, 'g'),
};
// Append all groups to the SVG and clear highlights on click.
Object.values(_this.g).forEach(function (group) { return _this.svg.appendChild(group); });
_this.svg.addEventListener('click', function () { return chart_1.Chart.clearChildren(_this.g.hilites); });
// Compute the axes limits and intervals
_this.maxFlow = 1.1 * SMath.clamp(_this.options.curve.pump.maxFlow, 0, Infinity);
_this.maxHead = 1.1 * SMath.clamp(_this.options.curve.pump.maxHead, 0, Infinity);
var flowStep = Pumpchart.getStep(_this.maxFlow, _this.options.size.x / _this.options.font.size / 6);
var headStep = Pumpchart.getStep(_this.maxHead, _this.options.size.y / _this.options.font.size / 3);
// Create the axes.
var xFlowAxis = _this.createPath([
{ flow: 0, head: 0 },
{ flow: _this.maxFlow, head: 0 },
]);
var yHeadAxis = _this.createPath([
{ flow: 0, head: 0 },
{ flow: 0, head: _this.maxHead },
]);
xFlowAxis.setAttribute('stroke', _this.options.axisColor);
yHeadAxis.setAttribute('stroke', _this.options.axisColor);
xFlowAxis.setAttribute('stroke-width', "".concat(_this.options.axisWidth * 2, "px"));
yHeadAxis.setAttribute('stroke-width', "".concat(_this.options.axisWidth * 2, "px"));
xFlowAxis.setAttribute('stroke-linecap', 'round');
yHeadAxis.setAttribute('stroke-linecap', 'round');
_this.g.axes.append(xFlowAxis, yHeadAxis);
for (var flow = flowStep; flow < _this.maxFlow; flow += flowStep) {
// Draw iso-flow vertical lines
var isoFlowLine = _this.createPath([
{ flow: flow, head: 0 },
{ flow: flow, head: _this.maxHead },
], false);
isoFlowLine.setAttribute('stroke', _this.options.axisColor);
isoFlowLine.setAttribute('stroke-width', _this.options.axisWidth + 'px');
isoFlowLine.setAttribute('stroke-linecap', 'round');
_this.g.axes.appendChild(isoFlowLine);
// Show axis label
_this.drawLabel("".concat(flow).concat(_this.options.units.flow), { flow: flow, head: 0 }, 2 /* TextAnchor.N */, "Flow [".concat(_this.options.units.flow, "]"));
}
for (var head = headStep; head < _this.maxHead; head += headStep) {
// Draw iso-head horizontal lines
var isoHeadLine = _this.createPath([
{ flow: 0, head: head },
{ flow: _this.maxFlow, head: head },
], false);
isoHeadLine.setAttribute('stroke', _this.options.axisColor);
isoHeadLine.setAttribute('stroke-width', _this.options.axisWidth + 'px');
isoHeadLine.setAttribute('stroke-linecap', 'round');
_this.g.axes.appendChild(isoHeadLine);
// Show axis label
_this.drawLabel("".concat(head).concat(_this.options.units.head), { flow: 0, head: head }, 4 /* TextAnchor.E */, "Head [".concat(_this.options.units.head, "]"));
}
// Draw the system curve
var sysColor = viridis_1.Color.hex(_this.options.systemCurveColor);
var nmax = SMath.clamp(_this.options.speed.max, 0, Infinity); // max speed
var nop = SMath.clamp(_this.options.speed.operation, 0, nmax); // operation speed
var qmax = (0, lib_1.zero)(function (q) { return _this.s(q) - _this.p(q, nmax); }, 0, 1e6); // flow @ max speed
var qop = (0, lib_1.zero)(function (q) { return _this.s(q) - _this.p(q, nop); }, 0, 1e6); // flow @ operation
var opPt = { flow: qop, head: _this.p(qop, nop), speed: nop }; // operation point
_this.drawCurve('System Curve', sysColor, 2 * _this.options.axisWidth, function (q) { return _this.s(q); }, 0, qmax);
// Draw operation axis lines
var operation = _this.createPath([
{ flow: 0, head: opPt.head },
opPt,
{ flow: opPt.flow, head: 0 },
], false);
operation.setAttribute('fill', 'none');
operation.setAttribute('stroke', sysColor.toString());
operation.setAttribute('stroke-width', "".concat(_this.options.axisWidth, "px"));
operation.setAttribute('stroke-linecap', 'round');
_this.g.curves.append(operation);
// Draw the operating point
_this.plot(opPt, {
name: 'Operation Point',
radius: 5 * _this.options.axisWidth,
color: _this.options.systemCurveColor,
});
// Draw concentric pump performance curves
var pumpColor = viridis_1.Color.hex(_this.options.pumpCurveColor);
_this.drawCurve("Performance Curve at ".concat(SMath.round2(nmax, 0.1)).concat(_this.options.units.speed), pumpColor, _this.options.axisWidth * 2, function (q) { return _this.p(q, nmax); });
_this.options.speed.steps.forEach(function (speed) {
_this.drawCurve('', pumpColor, _this.options.axisWidth, function (q) { return _this.p(q, speed); }, 0);
});
// Copy over the operation point to the curves layer
if (_this.g.data.lastChild) {
_this.g.curves.appendChild(_this.g.data.lastChild);
_this.clearData();
}
return _this;
}
/**
* Get the ideal axis interval
*/
Pumpchart.getStep = function (range, maxIntervals) {
var steps = [1, 2, 2.5, 5];
var magnitude = 1;
while (magnitude < 10) {
for (var _i = 0, steps_1 = steps; _i < steps_1.length; _i++) {
var stepi = steps_1[_i];
var step = stepi * (Math.pow(10, magnitude));
if (range / step < maxIntervals) {
return step;
}
}
magnitude++;
}
return 1;
};
/**
* Get the list of all available units for flow.
* @returns A list of units
*/
Pumpchart.getFlowUnits = function () {
return Object.keys(units_1.FlowUnits);
};
/**
* Get the list of all available units for head.
* @returns A list of units
*/
Pumpchart.getHeadUnits = function () {
return Object.keys(units_1.HeadUnits);
};
/**
* Get the list of all available units for speed.
* @returns A list of units
*/
Pumpchart.getSpeedUnits = function () {
return Object.keys(units_1.SpeedUnits);
};
/**
* Get the list of all available units for power.
* @returns A list of units
*/
Pumpchart.getPowerUnits = function () {
return Object.keys(units_1.PowerUnits);
};
/**
* Convert a state to an (x,y) coordinate.
* @param state Any state in this system
* @returns An (x,y) coordinate on the screen
*/
Pumpchart.prototype.state2xy = function (state) {
var xMin = this.options.padding.x;
var xMax = this.options.size.x - this.options.padding.x;
var yMin = this.options.padding.y;
var yMax = this.options.size.y - this.options.padding.y;
return {
x: SMath.clamp(SMath.translate(state.flow, 0, this.maxFlow, xMin, xMax), xMin, xMax),
y: SMath.clamp(SMath.translate(state.head, 0, this.maxHead, yMax, yMin), yMin, yMax),
};
};
/**
* Create a SVG path element from an array of states.
* @param data An array of states
* @param closePath Whether or not to close the path
* @returns A `<path>` element containing the array of states
*/
Pumpchart.prototype.createPath = function (data, closePath) {
var _this = this;
if (closePath === void 0) { closePath = false; }
var path = document.createElementNS(this.NS, 'path');
path.setAttribute('d', 'M ' + data.map(function (pt) {
var xy = _this.state2xy(pt);
return xy.x + ',' + xy.y;
}).join(' ') + (closePath ? ' z' : ''));
return path;
};
/**
* Draw an axis label
* @param content Label text content
* @param location Label location (state)
* @param color Label font color
* @param anchor Label text anchor
* @param tooltip Optional tooltip text on mouse hover
*/
Pumpchart.prototype.drawLabel = function (content, location, anchor, tooltip) {
var _this = this;
var textColor = viridis_1.Color.hex(this.options.textColor);
var label = this.createLabel(content, this.state2xy(location), textColor, anchor, 0);
this.g.text.appendChild(label);
if (tooltip) {
label.addEventListener('mouseover', function (e) { return _this.drawTooltip(tooltip, { x: e.offsetX, y: e.offsetY }, textColor, _this.g.tips); });
label.addEventListener('mouseleave', function () { return chart_1.Chart.clearChildren(_this.g.tips); });
}
};
/**
* Draw a curve `h = f(q)` on the curves layer.
*/
Pumpchart.prototype.drawCurve = function (tooltip, color, width, h, min, max, steps) {
var _this = this;
if (min === void 0) { min = 0; }
if (max === void 0) { max = this.maxFlow; }
if (steps === void 0) { steps = 1e3; }
var states = SMath.linspace(min, max, steps).map(function (q) { return { flow: q, head: h(q) }; });
var curve = this.createPath(states, false);
curve.setAttribute('fill', 'none');
curve.setAttribute('stroke', color.toString());
curve.setAttribute('stroke-width', "".concat(width, "px"));
curve.setAttribute('stroke-linecap', 'round');
this.g.curves.appendChild(curve);
if (tooltip) {
curve.addEventListener('mouseover', function (e) { return _this.drawTooltip(tooltip, { x: e.offsetX, y: e.offsetY }, color, _this.g.tips); });
curve.addEventListener('mouseleave', function () { return chart_1.Chart.clearChildren(_this.g.tips); });
}
};
/**
* Draw a custom circle onto any layer.
*/
Pumpchart.prototype.drawCircle = function (tooltip, color, state, radius, allowHighlight, parent) {
var _this = this;
var circle = document.createElementNS(this.NS, 'circle');
var center = this.state2xy(state);
circle.setAttribute('fill', color.toString());
circle.setAttribute('cx', "".concat(center.x, "px"));
circle.setAttribute('cy', "".concat(center.y, "px"));
circle.setAttribute('r', "".concat(radius, "px"));
parent.appendChild(circle);
if (tooltip) {
circle.addEventListener('mouseover', function (e) { return _this.drawTooltip(tooltip, { x: e.offsetX, y: e.offsetY }, color, _this.g.tips); });
circle.addEventListener('mouseleave', function () { return chart_1.Chart.clearChildren(_this.g.tips); });
}
if (allowHighlight) {
circle.addEventListener('click', function (e) {
e.stopPropagation();
_this.drawCircle('', viridis_1.Color.hex(_this.options.highlightColor), state, radius * 2, false, _this.g.hilites);
});
}
};
/**
* Represents the pump curve `h = p(q)`
* @param q Flow rate
* @param speed Pump speed
* @returns Head gained by the fluid by the pump
*/
Pumpchart.prototype.p = function (q, speed) {
var n = SMath.clamp(SMath.normalize(speed, 0, this.options.speed.max), 0.01, 1);
var h0 = SMath.clamp(this.options.curve.pump.maxHead * n, 0, Infinity);
var q0 = SMath.clamp(this.options.curve.pump.maxFlow * n, 0, Infinity);
return h0 * (1 - Math.pow((q / q0), 2));
};
/**
* Represents the system curve `h = s(q)`
* @param q Flow rate
* @returns Head loss from the fluid by the system
*/
Pumpchart.prototype.s = function (q) {
var h0 = SMath.clamp(this.options.curve.system.static, 0, Infinity);
var k = SMath.clamp(this.options.curve.system.friction, 0, Infinity);
return h0 + k * Math.pow(q, 2);
};
/**
* Plot a single data point.
* @param state The current state of the system
* @param config Display options for plotting data
*/
Pumpchart.prototype.plot = function (state, config) {
var _a;
var _this = this;
var _b;
if (config === void 0) { config = {}; }
var options = chart_1.Chart.setDefaults(config, defaults_1.defaultDataOptions);
var hasTimeStamp = Number.isFinite(options.timestamp);
var nmax = SMath.clamp(this.options.speed.max, 0, Infinity);
var speedEstimator = function (n) { return _this.p(state.flow, n) - state.head; };
var speed;
try {
speed = (_b = state.speed) !== null && _b !== void 0 ? _b : (0, lib_1.zero)(speedEstimator, 0, nmax);
}
catch (_c) {
speed = nmax;
}
// Calculate the efficiency if power is given
var efficiency = 0;
var output = 0;
if (typeof state.power === 'number') {
var headQty = new dimensional_1.Quantity(state.head, units_1.HeadUnits[this.options.units.head]);
if (units_1.HeadUnits[this.options.units.head].dimensions.is(dimensional_1.dimensions.Length)) {
// Need to multiply by specific weight to get the head in units of pressure
var specWeight = new dimensional_1.Quantity(SMath.clamp(this.options.specificGravity, 0, Infinity), dimensional_1.units.gram.over(dimensional_1.units.centimeter.pow(3)).times(dimensional_1.units.Gs));
headQty = headQty.times(specWeight);
}
// Efficiency = Power_{out} / Power_{in}
// Power_{out} = Pressure * FlowRate
var flowQty = new dimensional_1.Quantity(state.flow, units_1.FlowUnits[this.options.units.flow]);
var powQty = new dimensional_1.Quantity(state.power, units_1.PowerUnits[this.options.units.power]);
var eta = headQty.times(flowQty).over(powQty);
output = headQty.times(flowQty).as(powQty.units).quantity;
efficiency = eta.as(dimensional_1.units.Unitless).quantity * 100;
}
var tip = (options.name ? "".concat(options.name, "\n") : '') +
(hasTimeStamp ? "".concat(new Date(options.timestamp).toLocaleString(), "\n") : '') +
"Flow = ".concat(SMath.round2(state.flow, 0.1)).concat(this.options.units.flow) +
"\nHead = ".concat(SMath.round2(state.head, 0.1)).concat(this.options.units.head) +
"\nSpeed = ".concat(SMath.round2(speed, 0.1)).concat(this.options.units.speed).concat(typeof state.speed === 'number' ? '' : ' (est.)') +
(typeof state.power === 'number' ? ("\nPower = ".concat(SMath.round2(state.power, 0.1)).concat(this.options.units.power) +
"\nOutput = ".concat(SMath.round2(output, 0.1)).concat(this.options.units.power) +
"\nEfficiency = ".concat(SMath.round2(efficiency, 0.1), "%")) : '');
// Determine the assigned color for this data point
var gradientMin = 0;
var gradientMax = 0;
var gradientValue = 0;
var useGradient = false;
if (this.options.colorizeBy === 'time' && hasTimeStamp) {
gradientMin = this.options.timestamp.start;
gradientMax = this.options.timestamp.stop;
gradientValue = options.timestamp;
useGradient = true;
}
else if (this.options.colorizeBy === 'efficiency' && typeof state.power === 'number') {
gradientMin = 0;
gradientMax = 100;
gradientValue = efficiency;
useGradient = true;
}
if (useGradient && this.options.flipGradient) {
_a = [gradientMax, gradientMin], gradientMin = _a[0], gradientMax = _a[1];
}
var color = useGradient ? viridis_1.Palette[this.options.gradient].getColor(gradientValue, gradientMin, gradientMax) : viridis_1.Color.hex(options.color);
this.drawCircle(tip, color, state, options.radius, true, this.g.data);
};
/**
* Clear all the data from this chart.
*/
Pumpchart.prototype.clearData = function () {
chart_1.Chart.clearChildren(this.g.data);
chart_1.Chart.clearChildren(this.g.hilites);
};
return Pumpchart;
}(chart_1.Chart));
exports.Pumpchart = Pumpchart;