123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- "use strict";
- const { Task } = require("devtools/shared/task");
- const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
- const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
- const { LocalizationHelper } = require("devtools/shared/l10n");
- const HTML_NS = "http://www.w3.org/1999/xhtml";
- const L10N = new LocalizationHelper("devtools/client/locales/graphs.properties");
- // Line graph constants.
- const GRAPH_DAMPEN_VALUES_FACTOR = 0.85;
- // px
- const GRAPH_TOOLTIP_SAFE_BOUNDS = 8;
- const GRAPH_MIN_MAX_TOOLTIP_DISTANCE = 14;
- const GRAPH_BACKGROUND_COLOR = "#0088cc";
- // px
- const GRAPH_STROKE_WIDTH = 1;
- const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
- // px
- const GRAPH_HELPER_LINES_DASH = [5];
- const GRAPH_HELPER_LINES_WIDTH = 1;
- const GRAPH_MAXIMUM_LINE_COLOR = "rgba(255,255,255,0.4)";
- const GRAPH_AVERAGE_LINE_COLOR = "rgba(255,255,255,0.7)";
- const GRAPH_MINIMUM_LINE_COLOR = "rgba(255,255,255,0.9)";
- const GRAPH_BACKGROUND_GRADIENT_START = "rgba(255,255,255,0.25)";
- const GRAPH_BACKGROUND_GRADIENT_END = "rgba(255,255,255,0.0)";
- const GRAPH_CLIPHEAD_LINE_COLOR = "#fff";
- const GRAPH_SELECTION_LINE_COLOR = "#fff";
- const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)";
- const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
- const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
- const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";
- /**
- * A basic line graph, plotting values on a curve and adding helper lines
- * and tooltips for maximum, average and minimum values.
- *
- * @see AbstractCanvasGraph for emitted events and other options.
- *
- * Example usage:
- * let graph = new LineGraphWidget(node, "units");
- * graph.once("ready", () => {
- * graph.setData(src);
- * });
- *
- * Data source format:
- * [
- * { delta: x1, value: y1 },
- * { delta: x2, value: y2 },
- * ...
- * { delta: xn, value: yn }
- * ]
- * where each item in the array represents a point in the graph.
- *
- * @param nsIDOMNode parent
- * The parent node holding the graph.
- * @param object options [optional]
- * `metric`: The metric displayed in the graph, e.g. "fps" or "bananas".
- * `min`: Boolean whether to show the min tooltip/gutter/line (default: true)
- * `max`: Boolean whether to show the max tooltip/gutter/line (default: true)
- * `avg`: Boolean whether to show the avg tooltip/gutter/line (default: true)
- */
- this.LineGraphWidget = function (parent, options = {}, ...args) {
- let { metric, min, max, avg } = options;
- this._showMin = min !== false;
- this._showMax = max !== false;
- this._showAvg = avg !== false;
- AbstractCanvasGraph.apply(this, [parent, "line-graph", ...args]);
- this.once("ready", () => {
- // Create all gutters and tooltips incase the showing of min/max/avg
- // are changed later
- this._gutter = this._createGutter();
- this._maxGutterLine = this._createGutterLine("maximum");
- this._maxTooltip = this._createTooltip(
- "maximum", "start", L10N.getStr("graphs.label.maximum"), metric
- );
- this._minGutterLine = this._createGutterLine("minimum");
- this._minTooltip = this._createTooltip(
- "minimum", "start", L10N.getStr("graphs.label.minimum"), metric
- );
- this._avgGutterLine = this._createGutterLine("average");
- this._avgTooltip = this._createTooltip(
- "average", "end", L10N.getStr("graphs.label.average"), metric
- );
- });
- };
- LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
- backgroundColor: GRAPH_BACKGROUND_COLOR,
- backgroundGradientStart: GRAPH_BACKGROUND_GRADIENT_START,
- backgroundGradientEnd: GRAPH_BACKGROUND_GRADIENT_END,
- strokeColor: GRAPH_STROKE_COLOR,
- strokeWidth: GRAPH_STROKE_WIDTH,
- maximumLineColor: GRAPH_MAXIMUM_LINE_COLOR,
- averageLineColor: GRAPH_AVERAGE_LINE_COLOR,
- minimumLineColor: GRAPH_MINIMUM_LINE_COLOR,
- clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
- selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
- selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
- selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
- regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
- regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
- /**
- * Optionally offsets the `delta` in the data source by this scalar.
- */
- dataOffsetX: 0,
- /**
- * Optionally uses this value instead of the last tick in the data source
- * to compute the horizontal scaling.
- */
- dataDuration: 0,
- /**
- * The scalar used to multiply the graph values to leave some headroom.
- */
- dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,
- /**
- * Specifies if min/max/avg tooltips have arrow handlers on their sides.
- */
- withTooltipArrows: true,
- /**
- * Specifies if min/max/avg tooltips are positioned based on the actual
- * values, or just placed next to the graph corners.
- */
- withFixedTooltipPositions: false,
- /**
- * Takes a list of numbers and plots them on a line graph representing
- * the rate of occurences in a specified interval. Useful for drawing
- * framerate, for example, from a sequence of timestamps.
- *
- * @param array timestamps
- * A list of numbers representing time, ordered ascending. For example,
- * this can be the raw data received from the framerate actor, which
- * represents the elapsed time on each refresh driver tick.
- * @param number interval
- * The maximum amount of time to wait between calculations.
- * @param number duration
- * The duration of the recording in milliseconds.
- */
- setDataFromTimestamps: Task.async(function* (timestamps, interval, duration) {
- let {
- plottedData,
- plottedMinMaxSum
- } = yield CanvasGraphUtils._performTaskInWorker("plotTimestampsGraph", {
- timestamps, interval, duration
- });
- this._tempMinMaxSum = plottedMinMaxSum;
- this.setData(plottedData);
- }),
- /**
- * Renders the graph's data source.
- * @see AbstractCanvasGraph.prototype.buildGraphImage
- */
- buildGraphImage: function () {
- let { canvas, ctx } = this._getNamedCanvas("line-graph-data");
- let width = this._width;
- let height = this._height;
- let totalTicks = this._data.length;
- let firstTick = totalTicks ? this._data[0].delta : 0;
- let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
- let maxValue = Number.MIN_SAFE_INTEGER;
- let minValue = Number.MAX_SAFE_INTEGER;
- let avgValue = 0;
- if (this._tempMinMaxSum) {
- maxValue = this._tempMinMaxSum.maxValue;
- minValue = this._tempMinMaxSum.minValue;
- avgValue = this._tempMinMaxSum.avgValue;
- } else {
- let sumValues = 0;
- for (let { value } of this._data) {
- maxValue = Math.max(value, maxValue);
- minValue = Math.min(value, minValue);
- sumValues += value;
- }
- avgValue = sumValues / totalTicks;
- }
- let duration = this.dataDuration || lastTick;
- let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
- let dataScaleY =
- this.dataScaleY = height / maxValue * this.dampenValuesFactor;
- // Draw the background.
- ctx.fillStyle = this.backgroundColor;
- ctx.fillRect(0, 0, width, height);
- // Draw the graph.
- let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
- gradient.addColorStop(0, this.backgroundGradientStart);
- gradient.addColorStop(1, this.backgroundGradientEnd);
- ctx.fillStyle = gradient;
- ctx.strokeStyle = this.strokeColor;
- ctx.lineWidth = this.strokeWidth * this._pixelRatio;
- ctx.beginPath();
- for (let { delta, value } of this._data) {
- let currX = (delta - this.dataOffsetX) * dataScaleX;
- let currY = height - value * dataScaleY;
- if (delta == firstTick) {
- ctx.moveTo(-GRAPH_STROKE_WIDTH, height);
- ctx.lineTo(-GRAPH_STROKE_WIDTH, currY);
- }
- ctx.lineTo(currX, currY);
- if (delta == lastTick) {
- ctx.lineTo(width + GRAPH_STROKE_WIDTH, currY);
- ctx.lineTo(width + GRAPH_STROKE_WIDTH, height);
- }
- }
- ctx.fill();
- ctx.stroke();
- this._drawOverlays(ctx, minValue, maxValue, avgValue, dataScaleY);
- return canvas;
- },
- /**
- * Draws the min, max and average horizontal lines, along with their
- * repsective tooltips.
- *
- * @param CanvasRenderingContext2D ctx
- * @param number minValue
- * @param number maxValue
- * @param number avgValue
- * @param number dataScaleY
- */
- _drawOverlays: function (ctx, minValue, maxValue, avgValue, dataScaleY) {
- let width = this._width;
- let height = this._height;
- let totalTicks = this._data.length;
- // Draw the maximum value horizontal line.
- if (this._showMax) {
- ctx.strokeStyle = this.maximumLineColor;
- ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
- ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
- ctx.beginPath();
- let maximumY = height - maxValue * dataScaleY;
- ctx.moveTo(0, maximumY);
- ctx.lineTo(width, maximumY);
- ctx.stroke();
- }
- // Draw the average value horizontal line.
- if (this._showAvg) {
- ctx.strokeStyle = this.averageLineColor;
- ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
- ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
- ctx.beginPath();
- let averageY = height - avgValue * dataScaleY;
- ctx.moveTo(0, averageY);
- ctx.lineTo(width, averageY);
- ctx.stroke();
- }
- // Draw the minimum value horizontal line.
- if (this._showMin) {
- ctx.strokeStyle = this.minimumLineColor;
- ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
- ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
- ctx.beginPath();
- let minimumY = height - minValue * dataScaleY;
- ctx.moveTo(0, minimumY);
- ctx.lineTo(width, minimumY);
- ctx.stroke();
- }
- // Update the tooltips text and gutter lines.
- this._maxTooltip.querySelector("[text=value]").textContent =
- L10N.numberWithDecimals(maxValue, 2);
- this._avgTooltip.querySelector("[text=value]").textContent =
- L10N.numberWithDecimals(avgValue, 2);
- this._minTooltip.querySelector("[text=value]").textContent =
- L10N.numberWithDecimals(minValue, 2);
- let bottom = height / this._pixelRatio;
- let maxPosY = CanvasGraphUtils.map(maxValue * this.dampenValuesFactor, 0,
- maxValue, bottom, 0);
- let avgPosY = CanvasGraphUtils.map(avgValue * this.dampenValuesFactor, 0,
- maxValue, bottom, 0);
- let minPosY = CanvasGraphUtils.map(minValue * this.dampenValuesFactor, 0,
- maxValue, bottom, 0);
- let safeTop = GRAPH_TOOLTIP_SAFE_BOUNDS;
- let safeBottom = bottom - GRAPH_TOOLTIP_SAFE_BOUNDS;
- let maxTooltipTop = (this.withFixedTooltipPositions
- ? safeTop : CanvasGraphUtils.clamp(maxPosY, safeTop, safeBottom));
- let avgTooltipTop = (this.withFixedTooltipPositions
- ? safeTop : CanvasGraphUtils.clamp(avgPosY, safeTop, safeBottom));
- let minTooltipTop = (this.withFixedTooltipPositions
- ? safeBottom : CanvasGraphUtils.clamp(minPosY, safeTop, safeBottom));
- this._maxTooltip.style.top = maxTooltipTop + "px";
- this._avgTooltip.style.top = avgTooltipTop + "px";
- this._minTooltip.style.top = minTooltipTop + "px";
- this._maxGutterLine.style.top = maxPosY + "px";
- this._avgGutterLine.style.top = avgPosY + "px";
- this._minGutterLine.style.top = minPosY + "px";
- this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
- this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
- this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
- let distanceMinMax = Math.abs(maxTooltipTop - minTooltipTop);
- this._maxTooltip.hidden = this._showMax === false
- || !totalTicks
- || distanceMinMax < GRAPH_MIN_MAX_TOOLTIP_DISTANCE;
- this._avgTooltip.hidden = this._showAvg === false || !totalTicks;
- this._minTooltip.hidden = this._showMin === false || !totalTicks;
- this._gutter.hidden = (this._showMin === false &&
- this._showAvg === false &&
- this._showMax === false) || !totalTicks;
- this._maxGutterLine.hidden = this._showMax === false;
- this._avgGutterLine.hidden = this._showAvg === false;
- this._minGutterLine.hidden = this._showMin === false;
- },
- /**
- * Creates the gutter node when constructing this graph.
- * @return nsIDOMNode
- */
- _createGutter: function () {
- let gutter = this._document.createElementNS(HTML_NS, "div");
- gutter.className = "line-graph-widget-gutter";
- gutter.setAttribute("hidden", true);
- this._container.appendChild(gutter);
- return gutter;
- },
- /**
- * Creates the gutter line nodes when constructing this graph.
- * @return nsIDOMNode
- */
- _createGutterLine: function (type) {
- let line = this._document.createElementNS(HTML_NS, "div");
- line.className = "line-graph-widget-gutter-line";
- line.setAttribute("type", type);
- this._gutter.appendChild(line);
- return line;
- },
- /**
- * Creates the tooltip nodes when constructing this graph.
- * @return nsIDOMNode
- */
- _createTooltip: function (type, arrow, info, metric) {
- let tooltip = this._document.createElementNS(HTML_NS, "div");
- tooltip.className = "line-graph-widget-tooltip";
- tooltip.setAttribute("type", type);
- tooltip.setAttribute("arrow", arrow);
- tooltip.setAttribute("hidden", true);
- let infoNode = this._document.createElementNS(HTML_NS, "span");
- infoNode.textContent = info;
- infoNode.setAttribute("text", "info");
- let valueNode = this._document.createElementNS(HTML_NS, "span");
- valueNode.textContent = 0;
- valueNode.setAttribute("text", "value");
- let metricNode = this._document.createElementNS(HTML_NS, "span");
- metricNode.textContent = metric;
- metricNode.setAttribute("text", "metric");
- tooltip.appendChild(infoNode);
- tooltip.appendChild(valueNode);
- tooltip.appendChild(metricNode);
- this._container.appendChild(tooltip);
- return tooltip;
- }
- });
- module.exports = LineGraphWidget;
|