Bug 1174264 - Stacked Mountain Chart Widget, r=jsantell, a=Mossop
authorVictor Porof <vporof@mozilla.com>
Fri, 19 Jun 2015 19:26:27 -0400
changeset 280602 84df534bc80ecf16d31530373ea0f278ce263d8d
parent 280601 96669be6be1fd49491e4790959fabb9a46e3c4fd
child 280603 a452ab15ac37fa989603b742f6f08716e7293544
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell, Mossop
bugs1174264
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1174264 - Stacked Mountain Chart Widget, r=jsantell, a=Mossop
browser/devtools/shared/moz.build
browser/devtools/shared/test/browser.ini
browser/devtools/shared/test/browser_graphs-16.js
browser/devtools/shared/widgets/MountainGraphWidget.js
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -52,13 +52,14 @@ EXTRA_JS_MODULES.devtools.shared.widgets
     'widgets/CubicBezierPresets.js',
     'widgets/CubicBezierWidget.js',
     'widgets/FastListWidget.js',
     'widgets/FilterWidget.js',
     'widgets/FlameGraph.js',
     'widgets/Graphs.js',
     'widgets/LineGraphWidget.js',
     'widgets/MdnDocsWidget.js',
+    'widgets/MountainGraphWidget.js',
     'widgets/Spectrum.js',
     'widgets/TableWidget.js',
     'widgets/Tooltip.js',
     'widgets/TreeWidget.js',
 ]
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -68,19 +68,20 @@ support-files =
 [browser_graphs-10a.js]
 [browser_graphs-10b.js]
 [browser_graphs-10c.js]
 [browser_graphs-11a.js]
 [browser_graphs-11b.js]
 [browser_graphs-12.js]
 [browser_graphs-13.js]
 [browser_graphs-14.js]
+[browser_graphs-15.js]
+[browser_graphs-16.js]
 [browser_inplace-editor-01.js]
 [browser_inplace-editor-02.js]
-[browser_graphs-15.js]
 [browser_layoutHelpers.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_layoutHelpers-getBoxQuads.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_mdn-docs-01.js]
 [browser_mdn-docs-02.js]
 [browser_num-l10n.js]
 [browser_observableobject.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_graphs-16.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that mounta graphs work as expected.
+
+let MountainGraphWidget = devtools.require("devtools/shared/widgets/MountainGraphWidget");
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+const TEST_DATA = [
+  { delta: 0, values: [0.1, 0.5, 0.3] },
+  { delta: 1, values: [0.25, 0, 0.5] },
+  { delta: 2, values: [0.5, 0.25, 0.1] },
+  { delta: 3, values: [0, 0.75, 0] },
+  { delta: 4, values: [0.75, 0, 0.25] }
+];
+
+const SECTIONS = [
+  { color: "red" },
+  { color: "green" },
+  { color: "blue" }
+];
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  let graph = new MountainGraphWidget(doc.body);
+  yield graph.once("ready");
+
+  testGraph(graph);
+
+  yield graph.destroy();
+  host.destroy();
+}
+
+function testGraph(graph) {
+  graph.format = SECTIONS;
+  graph.setData(TEST_DATA);
+  ok(true, "The graph didn't throw any erorrs.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/MountainGraphWidget.js
@@ -0,0 +1,196 @@
+"use strict";
+
+const { Cc, Ci, Cu, Cr } = require("chrome");
+
+const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm");
+const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/shared/widgets/Graphs");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+// Bar graph constants.
+
+const GRAPH_DAMPEN_VALUES_FACTOR = 0.9;
+
+const GRAPH_BACKGROUND_COLOR = "#ddd";
+const GRAPH_STROKE_WIDTH = 2; // px
+const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
+const GRAPH_HELPER_LINES_DASH = [5]; // px
+const GRAPH_HELPER_LINES_WIDTH = 1; // px
+
+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 "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 = this._data[0].delta;
+    let lastTick = this._data[totalTicks - 1].delta;
+
+    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;