diff options
Diffstat (limited to 'devtools/client/shared/widgets/MountainGraphWidget.js')
-rw-r--r-- | devtools/client/shared/widgets/MountainGraphWidget.js | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/devtools/client/shared/widgets/MountainGraphWidget.js b/devtools/client/shared/widgets/MountainGraphWidget.js new file mode 100644 index 000000000..394ac4584 --- /dev/null +++ b/devtools/client/shared/widgets/MountainGraphWidget.js @@ -0,0 +1,195 @@ +"use strict"; + +const { Heritage } = require("devtools/client/shared/widgets/view-helpers"); +const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs"); + +// Bar graph constants. + +const GRAPH_DAMPEN_VALUES_FACTOR = 0.9; + +const GRAPH_BACKGROUND_COLOR = "#ddd"; +// 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_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 mountain graph, plotting sets of values as line graphs. + * + * @see AbstractCanvasGraph for emitted events and other options. + * + * Example usage: + * let graph = new MountainGraphWidget(node); + * graph.format = ...; + * graph.once("ready", () => { + * graph.setData(src); + * }); + * + * The `graph.format` traits are mandatory and will determine how each + * section of the moutain will be styled: + * [ + * { color: "#f00", ... }, + * { color: "#0f0", ... }, + * ... + * { color: "#00f", ... } + * ] + * + * Data source format: + * [ + * { delta: x1, values: [y11, y12, ... y1n] }, + * { delta: x2, values: [y21, y22, ... y2n] }, + * ... + * { delta: xm, values: [ym1, ym2, ... ymn] } + * ] + * where the [ymn] values is assumed to aready be normalized from [0..1]. + * + * @param nsIDOMNode parent + * The parent node holding the graph. + */ +this.MountainGraphWidget = function (parent, ...args) { + AbstractCanvasGraph.apply(this, [parent, "mountain-graph", ...args]); +}; + +MountainGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { + backgroundColor: GRAPH_BACKGROUND_COLOR, + strokeColor: GRAPH_STROKE_COLOR, + strokeWidth: GRAPH_STROKE_WIDTH, + 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, + + /** + * List of rules used to style each section of the mountain. + * @see constructor + * @type array + */ + format: null, + + /** + * 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 + * on the top. + */ + dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR, + + /** + * Renders the graph's background. + * @see AbstractCanvasGraph.prototype.buildBackgroundImage + */ + buildBackgroundImage: function () { + let { canvas, ctx } = this._getNamedCanvas("mountain-graph-background"); + let width = this._width; + let height = this._height; + + ctx.fillStyle = this.backgroundColor; + ctx.fillRect(0, 0, width, height); + + return canvas; + }, + + /** + * Renders the graph's data source. + * @see AbstractCanvasGraph.prototype.buildGraphImage + */ + buildGraphImage: function () { + if (!this.format || !this.format.length) { + throw new Error("The graph format traits are mandatory to style " + + "the data source."); + } + let { canvas, ctx } = this._getNamedCanvas("mountain-graph-data"); + let width = this._width; + let height = this._height; + + let totalSections = this.format.length; + let totalTicks = this._data.length; + let firstTick = totalTicks ? this._data[0].delta : 0; + let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0; + + let duration = this.dataDuration || lastTick; + let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX); + let dataScaleY = this.dataScaleY = height * this.dampenValuesFactor; + + // Draw the graph. + + let prevHeights = Array.from({ length: totalTicks }).fill(0); + + ctx.globalCompositeOperation = "destination-over"; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth * this._pixelRatio; + + for (let section = 0; section < totalSections; section++) { + ctx.fillStyle = this.format[section].color || "#000"; + ctx.beginPath(); + + for (let tick = 0; tick < totalTicks; tick++) { + let { delta, values } = this._data[tick]; + let currX = (delta - this.dataOffsetX) * dataScaleX; + let currY = values[section] * dataScaleY; + let prevY = prevHeights[tick]; + + if (delta == firstTick) { + ctx.moveTo(-GRAPH_STROKE_WIDTH, height); + ctx.lineTo(-GRAPH_STROKE_WIDTH, height - currY - prevY); + } + + ctx.lineTo(currX, height - currY - prevY); + + if (delta == lastTick) { + ctx.lineTo(width + GRAPH_STROKE_WIDTH, height - currY - prevY); + ctx.lineTo(width + GRAPH_STROKE_WIDTH, height); + } + + prevHeights[tick] += currY; + } + + ctx.fill(); + ctx.stroke(); + } + + ctx.globalCompositeOperation = "source-over"; + ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH; + ctx.setLineDash(GRAPH_HELPER_LINES_DASH); + + // Draw the maximum value horizontal line. + + ctx.beginPath(); + let maximumY = height * this.dampenValuesFactor; + ctx.moveTo(0, maximumY); + ctx.lineTo(width, maximumY); + ctx.stroke(); + + // Draw the average value horizontal line. + + ctx.beginPath(); + let averageY = height / 2 * this.dampenValuesFactor; + ctx.moveTo(0, averageY); + ctx.lineTo(width, averageY); + ctx.stroke(); + + return canvas; + } +}); + +module.exports = MountainGraphWidget; |