summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/widgets/LineGraphWidget.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/widgets/LineGraphWidget.js')
-rw-r--r--devtools/client/shared/widgets/LineGraphWidget.js402
1 files changed, 402 insertions, 0 deletions
diff --git a/devtools/client/shared/widgets/LineGraphWidget.js b/devtools/client/shared/widgets/LineGraphWidget.js
new file mode 100644
index 000000000..12ca425ad
--- /dev/null
+++ b/devtools/client/shared/widgets/LineGraphWidget.js
@@ -0,0 +1,402 @@
+"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;