/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ function compStyle(property) { return getComputedStyle(window.document.body).getPropertyValue(property); } function toHumanReadable(num, fpDecimals) { const prefixes = [..." kMGTPEYZYRQ"]; const inner = (curr, remainingPrefixes) => { return Math.abs(curr >= 1000) ? inner(curr / 1000, remainingPrefixes.slice(1, -1)) : [curr.toFixed(fpDecimals), remainingPrefixes[0].trimEnd()]; }; return inner(num, prefixes); } class GraphImpl { constructor(width, height) { this.width = width; this.height = height; } // The returns the earliest time to graph startTime = dataSet => (dataSet.earliest() || { time: 0 }).time; // Returns the latest time to graph stopTime = dataSet => (dataSet.latest() || { time: 0 }).time; // The default background color bgColor = () => compStyle("--in-content-page-background"); // The color to use for value graph lines valueLineColor = () => "grey"; // The color to use for average graph lines and text averageLineColor = () => "green"; // The color to use for the max value maxColor = ({ time, value }) => "grey"; // The color to use for the min value minColor = ({ time, value }) => "grey"; // Title color titleColor = title => compStyle("--in-content-page-color"); // The color to use for a data point at a time. // The destination x coordinate and graph width are also provided. datumColor = ({ time, value, x, width }) => "red"; // Returns an SVG element that needs to be inserted into the DOM for display drawSparseValues = (dataSet, title, config) => { const { width, height } = this; // Clear the canvas const bgColor = this.bgColor(); const mkSvgElem = type => document.createElementNS("http://www.w3.org/2000/svg", type); const svgText = (x, y, text, color, subclass) => { const txt = mkSvgElem("text"); txt.setAttribute("x", x); txt.setAttribute("y", y); txt.setAttribute("stroke", bgColor); txt.setAttribute("fill", color); txt.setAttribute("paint-order", "stroke"); txt.textContent = text; txt.classList.add(["graph-text", ...[subclass]].join("-")); return txt; }; const svg = mkSvgElem("svg"); svg.setAttribute("viewBox", `0 0 ${width} ${height}`); svg.setAttribute("version", "1.1"); svg.setAttribute("width", width); svg.setAttribute("height", height); svg.classList.add("svg-graph"); const rect = mkSvgElem("rect"); rect.setAttribute("fill", bgColor); rect.setAttribute("width", width); rect.setAttribute("height", height); svg.appendChild(rect); if (config.toRate) { dataSet = dataSet.toRateDataSet(); } const startTime = this.startTime(dataSet); const stopTime = this.stopTime(dataSet); let timeFilter = ({ time }) => time >= startTime && time <= stopTime; let avgDataSet = { dataPoints: [] }; if (!config.noAvg) { avgDataSet = dataSet.toRollingAverageDataSet(config.avgPoints); } let filtered = dataSet.filter(timeFilter); if (filtered.dataPoints == []) { return svg; } let range = filtered.dataRange(); if (range === undefined) { return svg; } let { min: rangeMin, max: rangeMax } = range; // Adjust the _display_ range to lift flat lines towards the center if (rangeMin == rangeMax) { rangeMin = rangeMin - 1; rangeMax = rangeMax + 1; } const yFactor = (height - 26) / (1 + rangeMax - rangeMin); const yPos = ({ value }) => this.height - 1 - (value - rangeMin) * yFactor - 13; const xFactor = width / (1 + stopTime - startTime); const xPos = ({ time }) => (time - startTime) * xFactor; const toPathStr = dataPoints => [...dataPoints] .map( (datum, index) => `${index ? "L" : "M"}${xPos(datum)} ${yPos(datum)}` ) .join(" "); const valuePath = mkSvgElem("path"); valuePath.setAttribute("d", toPathStr(filtered.dataPoints)); valuePath.setAttribute("stroke", this.valueLineColor()); valuePath.setAttribute("fill", "none"); svg.appendChild(valuePath); const avgPath = mkSvgElem("path"); avgPath.setAttribute("d", toPathStr(avgDataSet.dataPoints)); avgPath.setAttribute("stroke", this.averageLineColor()); avgPath.setAttribute("fill", "none"); svg.appendChild(avgPath); const fixed = num => num.toFixed(config.fixedPointDecimals); const formatValue = value => config.toHuman ? toHumanReadable(value, config.fixedPointDecimals).join("") : fixed(value); // Draw rolling average text avgDataSet.dataPoints.slice(-1).forEach(({ value }) => { svg.appendChild( svgText( 5, height - 4, `AVG: ${formatValue(value)}`, this.averageLineColor(), "avg" ) ); }); // Draw title text if (title) { svg.appendChild( svgText( 5, 12, `${title}${config.toRate ? "/s" : ""}`, this.titleColor(this), "title" ) ); } // Draw max value text const maxText = svgText( width - 5, 12, `Max: ${formatValue(range.max)}`, this.maxColor(range.max), "max" ); maxText.setAttribute("text-anchor", "end"); svg.appendChild(maxText); // Draw min value text const minText = svgText( width - 5, height - 4, `Min: ${formatValue(range.min)}`, this.minColor(range.min), "min" ); minText.setAttribute("text-anchor", "end"); svg.appendChild(minText); return svg; }; } export { GraphImpl };