diff --git a/browser/devtools/shared/test/browser.ini b/browser/devtools/shared/test/browser.ini index 0a6e20584a4..16768895b2e 100644 --- a/browser/devtools/shared/test/browser.ini +++ b/browser/devtools/shared/test/browser.ini @@ -26,7 +26,8 @@ support-files = [browser_graphs-09.js] [browser_graphs-10a.js] [browser_graphs-10b.js] -[browser_graphs-11.js] +[browser_graphs-11a.js] +[browser_graphs-11b.js] [browser_graphs-12.js] [browser_graphs-13.js] [browser_graphs-14.js] diff --git a/browser/devtools/shared/test/browser_graphs-02.js b/browser/devtools/shared/test/browser_graphs-02.js index cd599095b26..0db26211538 100644 --- a/browser/devtools/shared/test/browser_graphs-02.js +++ b/browser/devtools/shared/test/browser_graphs-02.js @@ -1,7 +1,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests that graph widgets can properly add data and regions. +// Tests that graph widgets can properly add data, regions and highlights. const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }]; const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }]; @@ -22,13 +22,14 @@ function* performTest() { let graph = new LineGraphWidget(doc.body, "fps"); yield graph.once("ready"); - testGraph(graph); + testDataAndRegions(graph); + testHighlights(graph); graph.destroy(); host.destroy(); } -function testGraph(graph) { +function testDataAndRegions(graph) { let thrown1; try { graph.setRegions(TEST_REGIONS); @@ -52,7 +53,7 @@ function testGraph(graph) { ok(graph.hasRegions(), "The graph should now have the regions set."); is(graph.dataScaleX, - graph.width / (4180 - 112), // last & first tick in TEST_DATA + graph.width / 4180, // last & first tick in TEST_DATA "The data scale on the X axis is correct."); is(graph.dataScaleY, @@ -69,3 +70,17 @@ function testGraph(graph) { "The region's end value was properly normalized."); } } + +function testHighlights(graph) { + graph.setMask(TEST_REGIONS); + ok(graph.hasMask(), + "The graph should now have the highlights set."); + + graph.setMask([]); + ok(graph.hasMask(), + "The graph shouldn't have anything highlighted."); + + graph.setMask(null); + ok(!graph.hasMask(), + "The graph should have everything highlighted."); +} diff --git a/browser/devtools/shared/test/browser_graphs-11.js b/browser/devtools/shared/test/browser_graphs-11a.js similarity index 100% rename from browser/devtools/shared/test/browser_graphs-11.js rename to browser/devtools/shared/test/browser_graphs-11a.js diff --git a/browser/devtools/shared/test/browser_graphs-11b.js b/browser/devtools/shared/test/browser_graphs-11b.js new file mode 100644 index 00000000000..9aedb314018 --- /dev/null +++ b/browser/devtools/shared/test/browser_graphs-11b.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that bar graph's legend items handle mouseover/mouseout. + +let {BarGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {}); +let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {}); +let {Promise} = devtools.require("resource://gre/modules/Promise.jsm"); +let {Hosts} = devtools.require("devtools/framework/toolbox-hosts"); + +const CATEGORIES = [ + { color: "#46afe3", label: "Foo" }, + { color: "#eb5368", label: "Bar" }, + { color: "#70bf53", label: "Baz" } +]; + +let test = Task.async(function*() { + yield promiseTab("about:blank"); + yield performTest(); + gBrowser.removeCurrentTab(); + finish(); +}); + +function* performTest() { + let [host, win, doc] = yield createHost(); + doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;"); + + let graph = new BarGraphWidget(doc.body, 1); + graph.fixedWidth = 200; + graph.fixedHeight = 100; + + yield graph.once("ready"); + yield testGraph(graph); + + graph.destroy(); + host.destroy(); +} + +function* testGraph(graph) { + graph.format = CATEGORIES; + graph.dataOffsetX = 1000; + graph.setData([{ + delta: 1100, values: [0, 2, 3] + }, { + delta: 1200, values: [1, 0, 2] + }, { + delta: 1300, values: [2, 1, 0] + }, { + delta: 1400, values: [0, 3, 1] + }, { + delta: 1500, values: [3, 0, 2] + }, { + delta: 1600, values: [3, 2, 0] + }]); + + is(graph._blocksBoundingRects.toSource(), "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]", + "The correct blocks bounding rects were calculated for the bar graph."); + + let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item"); + is(legendItems.length, 3, + "Three legend items should exist in the entire graph."); + + yield testLegend(graph, 0, { + highlights: "[{type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}]", + selection: "({start:34.33333333333333, end:200})", + leftmost: "({type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100})", + rightmost: "({type:0, start:167.66666666666666, end:200, top:55, bottom:100})" + }); + yield testLegend(graph, 1, { + highlights: "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]", + selection: "({start:0, end:200})", + leftmost: "({type:1, start:0, end:33.33333333333333, top:70, bottom:100})", + rightmost: "({type:1, start:167.66666666666666, end:200, top:24, bottom:54})" + }); + yield testLegend(graph, 2, { + highlights: "[{type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}]", + selection: "({start:0, end:166.66666666666666})", + leftmost: "({type:2, start:0, end:33.33333333333333, top:24, bottom:69})", + rightmost: "({type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54})" + }); +} + +function* testLegend(graph, index, { highlights, selection, leftmost, rightmost }) { + // Hover. + + let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item"); + let colorBlock = legendItems[index].querySelector("[view=color]"); + + let debounced = graph.once("legend-hover"); + graph._onLegendMouseOver({ target: colorBlock }); + ok(!graph.hasMask(), "The graph shouldn't get highlights immediately."); + + let [type, rects] = yield debounced; + ok(graph.hasMask(), "The graph should now have highlights."); + + is(type, index, + "The legend item was correctly hovered."); + is(rects.toSource(), highlights, + "The legend item highlighted the correct regions."); + + // Unhover. + + let unhovered = graph.once("legend-unhover"); + graph._onLegendMouseOut(); + ok(!graph.hasMask(), "The graph shouldn't have highlights anymore."); + + yield unhovered; + ok(true, "The 'legend-mouseout' event was emitted."); + + // Select. + + let selected = graph.once("legend-selection"); + graph._onLegendMouseDown(mockEvent(colorBlock)); + ok(graph.hasSelection(), "The graph should now have a selection."); + is(graph.getSelection().toSource(), selection, "The graph has a correct selection."); + + let [left, right] = yield selected; + is(left.toSource(), leftmost, "The correct leftmost data block was found."); + is(right.toSource(), rightmost, "The correct rightmost data block was found."); + + // Deselect. + + graph.dropSelection(); +} + +function mockEvent(node) { + return { + target: node, + preventDefault: () => {}, + stopPropagation: () => {} + }; +} diff --git a/browser/devtools/shared/widgets/Graphs.jsm b/browser/devtools/shared/widgets/Graphs.jsm index a80e1830169..7ee49e8c711 100644 --- a/browser/devtools/shared/widgets/Graphs.jsm +++ b/browser/devtools/shared/widgets/Graphs.jsm @@ -76,6 +76,11 @@ const BAR_GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)"; const BAR_GRAPH_REGION_BACKGROUND_COLOR = "transparent"; const BAR_GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)"; +const BAR_GRAPH_HIGHLIGHTS_MASK_BACKGROUND = "rgba(255,255,255,0.75)"; +const BAR_GRAPH_HIGHLIGHTS_MASK_STRIPES = "rgba(255,255,255,0.5)"; + +const BAR_GRAPH_LEGEND_MOUSEOVER_DEBOUNCE = 50; // ms + /** * Small data primitives for all graphs. */ @@ -232,11 +237,17 @@ AbstractCanvasGraph.prototype = { this._iframe.remove(); this._data = null; + this._mask = null; + this._maskArgs = null; this._regions = null; + this._cachedBackgroundImage = null; this._cachedGraphImage = null; + this._cachedMaskImage = null; this._renderTargets.clear(); gCachedStripePattern.clear(); + + this.emit("destroyed"); }, /** @@ -275,6 +286,15 @@ AbstractCanvasGraph.prototype = { throw "This method needs to be implemented by inheriting classes."; }, + /** + * Optionally builds and caches a mask image for this graph, composited + * over the data image created via `buildGraphImage`. Inheriting classes + * may override this method. + */ + buildMaskImage: function() { + return null; + }, + /** * When setting the data source, the coordinates and values may be * stretched or squeezed on the X/Y axis, to fit into the available space. @@ -308,6 +328,19 @@ AbstractCanvasGraph.prototype = { this.setData(data); }), + /** + * Adds a mask to this graph. + * + * @param any mask, options + * See `buildMaskImage` in inheriting classes for the required args. + */ + setMask: function(mask, ...options) { + this._mask = mask; + this._maskArgs = [mask, ...options]; + this._cachedMaskImage = this.buildMaskImage.apply(this, this._maskArgs); + this._shouldRedraw = true; + }, + /** * Adds regions to this graph. * @@ -340,6 +373,14 @@ AbstractCanvasGraph.prototype = { return !!this._data; }, + /** + * Gets whether or not this graph has any mask applied. + * @return boolean + */ + hasMask: function() { + return !!this._mask; + }, + /** * Gets whether or not this graph has any regions. * @return boolean @@ -582,6 +623,9 @@ AbstractCanvasGraph.prototype = { this._cachedBackgroundImage = this.buildBackgroundImage(); this._cachedGraphImage = this.buildGraphImage(); } + if (this.hasMask()) { + this._cachedMaskImage = this.buildMaskImage.apply(this, this._maskArgs); + } if (this.hasRegions()) { this._bakeRegions(this._regions, this._cachedGraphImage); } @@ -648,12 +692,22 @@ AbstractCanvasGraph.prototype = { let ctx = this._ctx; ctx.clearRect(0, 0, this._width, this._height); - if (this._cachedBackgroundImage) { - ctx.drawImage(this._cachedBackgroundImage, 0, 0, this._width, this._height); - } if (this._cachedGraphImage) { ctx.drawImage(this._cachedGraphImage, 0, 0, this._width, this._height); } + if (this._cachedMaskImage) { + ctx.globalCompositeOperation = "destination-out"; + ctx.drawImage(this._cachedMaskImage, 0, 0, this._width, this._height); + } + if (this._cachedBackgroundImage) { + ctx.globalCompositeOperation = "destination-over"; + ctx.drawImage(this._cachedBackgroundImage, 0, 0, this._width, this._height); + } + + // Revert to the original global composition operation. + if (this._cachedMaskImage || this._cachedBackgroundImage) { + ctx.globalCompositeOperation = "source-over"; + } if (this.hasCursor()) { this._drawCliphead(); @@ -1112,6 +1166,11 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { regionBackgroundColor: LINE_GRAPH_REGION_BACKGROUND_COLOR, regionStripesColor: LINE_GRAPH_REGION_STRIPES_COLOR, + /** + * Optionally offsets the `delta` in the data source by this scalar. + */ + dataOffsetX: 0, + /** * Points that are too close too each other in the graph will not be rendered. * This scalar specifies the required minimum squared distance between points. @@ -1119,7 +1178,7 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { minDistanceBetweenPoints: LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS, /** - * Renders the graph on a canvas. + * Renders the graph's data source. * @see AbstractCanvasGraph.prototype.buildGraphImage */ buildGraphImage: function() { @@ -1140,7 +1199,7 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { sumValues += value; } - let dataScaleX = this.dataScaleX = width / (lastTick - firstTick); + let dataScaleX = this.dataScaleX = width / (lastTick - this.dataOffsetX); let dataScaleY = this.dataScaleY = height / maxValue * LINE_GRAPH_DAMPEN_VALUES; /** @@ -1166,7 +1225,7 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { let prevY = 0; for (let { delta, value } of this._data) { - let currX = (delta - firstTick) * dataScaleX; + let currX = (delta - this.dataOffsetX) * dataScaleX; let currY = height - value * dataScaleY; if (delta == firstTick) { @@ -1351,9 +1410,24 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { this.BarGraphWidget = function(parent, ...args) { AbstractCanvasGraph.apply(this, [parent, "bar-graph", ...args]); + // Populated with [node, event, listener] entries which need to be removed + // when this graph is being destroyed. + this.outstandingEventListeners = []; + this.once("ready", () => { + this._onLegendMouseOver = this._onLegendMouseOver.bind(this); + this._onLegendMouseOut = this._onLegendMouseOut.bind(this); + this._onLegendMouseDown = this._onLegendMouseDown.bind(this); + this._onLegendMouseUp = this._onLegendMouseUp.bind(this); this._createLegend(); }); + + this.once("destroyed", () => { + for (let [node, event, listener] of this.outstandingEventListeners) { + node.removeEventListener(event, listener); + } + this.outstandingEventListeners = null; + }); } BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { @@ -1370,6 +1444,11 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { */ format: null, + /** + * Optionally offsets the `delta` in the data source by this scalar. + */ + dataOffsetX: 0, + /** * Bars that are too close too each other in the graph will be combined. * This scalar specifies the required minimum width of each bar. @@ -1401,7 +1480,7 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { }, /** - * Renders the graph on a canvas. + * Renders the graph's data source. * @see AbstractCanvasGraph.prototype.buildGraphImage */ buildGraphImage: function() { @@ -1414,17 +1493,15 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { let totalTypes = this.format.length; let totalTicks = this._data.length; - let firstTick = this._data[0].delta; let lastTick = this._data[totalTicks - 1].delta; let minBarsWidth = this.minBarsWidth * this._pixelRatio; let minBlocksHeight = this.minBlocksHeight * this._pixelRatio; - let dataScaleX = this.dataScaleX = width / (lastTick - firstTick); + let dataScaleX = this.dataScaleX = width / (lastTick - this.dataOffsetX); let dataScaleY = this.dataScaleY = height / this._calcMaxHeight({ data: this._data, dataScaleX: dataScaleX, - dataOffsetX: firstTick, minBarsWidth: minBarsWidth }) * BAR_GRAPH_DAMPEN_VALUES; @@ -1434,6 +1511,7 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { // the same color in a single pass. See the @constructor for more // information about the data source, and how a "bar" contains "blocks". + this._blocksBoundingRects = []; let prevHeight = []; let scaledMarginEnd = BAR_GRAPH_BARS_MARGIN_END * this._pixelRatio; let unscaledMarginTop = BAR_GRAPH_BARS_MARGIN_TOP; @@ -1442,17 +1520,17 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { ctx.fillStyle = this.format[type].color || "#000"; ctx.beginPath(); - let prevLeft = 0; + let prevRight = 0; let skippedCount = 0; let skippedHeight = 0; for (let tick = 0; tick < totalTicks; tick++) { let delta = this._data[tick].delta; let value = this._data[tick].values[type] || 0; - let blockLeft = (delta - firstTick) * dataScaleX; + let blockRight = (delta - this.dataOffsetX) * dataScaleX; let blockHeight = value * dataScaleY; - let blockWidth = blockLeft - prevLeft; + let blockWidth = blockRight - prevRight; if (blockWidth < minBarsWidth) { skippedCount++; skippedHeight += blockHeight; @@ -1462,10 +1540,19 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { let averageHeight = (blockHeight + skippedHeight) / (skippedCount + 1); if (averageHeight >= minBlocksHeight) { let bottom = height - ~~prevHeight[tick]; - ctx.moveTo(prevLeft, bottom); - ctx.lineTo(prevLeft, bottom - averageHeight); - ctx.lineTo(blockLeft, bottom - averageHeight); - ctx.lineTo(blockLeft, bottom); + ctx.moveTo(prevRight, bottom); + ctx.lineTo(prevRight, bottom - averageHeight); + ctx.lineTo(blockRight, bottom - averageHeight); + ctx.lineTo(blockRight, bottom); + + // Remember this block's type and location. + this._blocksBoundingRects.push({ + type: type, + start: prevRight, + end: blockRight, + top: bottom - averageHeight, + bottom: bottom + }); if (prevHeight[tick] === undefined) { prevHeight[tick] = averageHeight + unscaledMarginTop; @@ -1474,7 +1561,7 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { } } - prevLeft += blockWidth + scaledMarginEnd; + prevRight += blockWidth + scaledMarginEnd; skippedHeight = 0; skippedCount = 0; } @@ -1482,6 +1569,11 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { ctx.fill(); } + // The blocks bounding rects isn't guaranteed to be sorted ascending by + // block location on the X axis. This should be the case, for better + // cache cohesion and a faster `buildMaskImage`. + this._blocksBoundingRects.sort((a, b) => a.start > b.start ? 1 : -1); + // Update the legend. while (this._legendNode.hasChildNodes()) { @@ -1494,6 +1586,74 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { return canvas; }, + /** + * Renders the graph's mask. + * Fades in only the parts of the graph that are inside the specified areas. + * + * @param array highlights + * A list of { start, end } values. Optionally, each object + * in the list may also specify { top, bottom } pixel values if the + * highlighting shouldn't span across the full height of the graph. + * @param boolean inPixels + * Set this to true if the { start, end } values in the highlights + * list are pixel values, and not values from the data source. + * @param function unpack [optional] + * @see AbstractCanvasGraph.prototype.getMappedSelection + */ + buildMaskImage: function(highlights, inPixels = false, unpack = e => e.delta) { + // A null `highlights` array is used to clear the mask. An empty array + // will mask the entire graph. + if (!highlights) { + return null; + } + + // Get a render target for the highlights. It will be overlaid on top of + // the existing graph, masking the areas that aren't highlighted. + + let { canvas, ctx } = this._getNamedCanvas("graph-highlights"); + let width = this._width; + let height = this._height; + + // Draw the background mask. + + let pattern = AbstractCanvasGraph.getStripePattern({ + ownerDocument: this._document, + backgroundColor: BAR_GRAPH_HIGHLIGHTS_MASK_BACKGROUND, + stripesColor: BAR_GRAPH_HIGHLIGHTS_MASK_STRIPES + }); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, width, height); + + // Clear highlighted areas. + + let totalTicks = this._data.length; + let firstTick = unpack(this._data[0]); + let lastTick = unpack(this._data[totalTicks - 1]); + + for (let { start, end, top, bottom } of highlights) { + if (!inPixels) { + start = map(start, firstTick, lastTick, 0, width); + end = map(end, firstTick, lastTick, 0, width); + } + let firstSnap = findFirst(this._blocksBoundingRects, e => e.start >= start); + let lastSnap = findLast(this._blocksBoundingRects, e => e.start >= start && e.end <= end); + + let x1 = firstSnap ? firstSnap.start : start; + let x2 = lastSnap ? lastSnap.end : firstSnap ? firstSnap.end : end; + let y1 = top || 0; + let y2 = bottom || height; + ctx.clearRect(x1, y1, x2 - x1, y2 - y1); + } + + return canvas; + }, + + /** + * A list storing the bounding rectangle for each drawn block in the graph. + * Created whenever `buildGraphImage` is invoked. + */ + _blocksBoundingRects: null, + /** * Calculates the height of the tallest bar that would eventially be rendered * in this graph. @@ -1504,18 +1664,18 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { * @return number * The tallest bar height in this graph. */ - _calcMaxHeight: function({ data, dataScaleX, dataOffsetX, minBarsWidth }) { + _calcMaxHeight: function({ data, dataScaleX, minBarsWidth }) { let maxHeight = 0; - let prevLeft = 0; + let prevRight = 0; let skippedCount = 0; let skippedHeight = 0; let scaledMarginEnd = BAR_GRAPH_BARS_MARGIN_END * this._pixelRatio; for (let { delta, values } of data) { - let barLeft = (delta - dataOffsetX) * dataScaleX; + let barRight = (delta - this.dataOffsetX) * dataScaleX; let barHeight = values.reduce((a, b) => a + b, 0); - let barWidth = barLeft - prevLeft; + let barWidth = barRight - prevRight; if (barWidth < minBarsWidth) { skippedCount++; skippedHeight += barHeight; @@ -1525,7 +1685,7 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { let averageHeight = (barHeight + skippedHeight) / (skippedCount + 1); maxHeight = Math.max(averageHeight, maxHeight); - prevLeft += barWidth + scaledMarginEnd; + prevRight += barWidth + scaledMarginEnd; skippedHeight = 0; skippedCount = 0; } @@ -1551,7 +1711,17 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { let colorNode = this._document.createElementNS(HTML_NS, "span"); colorNode.setAttribute("view", "color"); + colorNode.setAttribute("data-index", this._legendNode.childNodes.length); colorNode.style.backgroundColor = color; + colorNode.addEventListener("mouseover", this._onLegendMouseOver); + colorNode.addEventListener("mouseout", this._onLegendMouseOut); + colorNode.addEventListener("mousedown", this._onLegendMouseDown); + colorNode.addEventListener("mouseup", this._onLegendMouseUp); + + this.outstandingEventListeners.push([colorNode, "mouseover", this._onLegendMouseOver]); + this.outstandingEventListeners.push([colorNode, "mouseout", this._onLegendMouseOut]); + this.outstandingEventListeners.push([colorNode, "mousedown", this._onLegendMouseDown]); + this.outstandingEventListeners.push([colorNode, "mouseup", this._onLegendMouseUp]); let labelNode = this._document.createElementNS(HTML_NS, "span"); labelNode.setAttribute("view", "label"); @@ -1560,6 +1730,65 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { itemNode.appendChild(colorNode); itemNode.appendChild(labelNode); this._legendNode.appendChild(itemNode); + }, + + /** + * Invoked whenever a color node in the legend is hovered. + */ + _onLegendMouseOver: function(e) { + setNamedTimeout("bar-graph-debounce", BAR_GRAPH_LEGEND_MOUSEOVER_DEBOUNCE, () => { + let type = e.target.dataset.index; + let rects = this._blocksBoundingRects.filter(e => e.type == type); + + this._originalHighlights = this._mask; + this._hasCustomHighlights = true; + this.setMask(rects, true); + + this.emit("legend-hover", [type, rects]); + }); + }, + + /** + * Invoked whenever a color node in the legend is unhovered. + */ + _onLegendMouseOut: function() { + clearNamedTimeout("bar-graph-debounce"); + + if (this._hasCustomHighlights) { + this.setMask(this._originalHighlights); + this._hasCustomHighlights = false; + this._originalHighlights = null; + } + + this.emit("legend-unhover"); + }, + + /** + * Invoked whenever a color node in the legend is pressed. + */ + _onLegendMouseDown: function(e) { + e.preventDefault(); + e.stopPropagation(); + + let type = e.target.dataset.index; + let rects = this._blocksBoundingRects.filter(e => e.type == type); + let leftmost = rects[0]; + let rightmost = rects[rects.length - 1]; + if (!leftmost || !rightmost) { + this.dropSelection(); + } else { + this.setSelection({ start: leftmost.start, end: rightmost.end }); + } + + this.emit("legend-selection", [leftmost, rightmost]); + }, + + /** + * Invoked whenever a color node in the legend is released. + */ + _onLegendMouseUp: function(e) { + e.preventDefault(); + e.stopPropagation(); } }); @@ -1694,7 +1923,35 @@ this.CanvasGraphUtils = { /** * Maps a value from one range to another. + * @param number value, istart, istop, ostart, ostop + * @return number */ function map(value, istart, istop, ostart, ostop) { return ostart + (ostop - ostart) * ((value - istart) / (istop - istart)); } + +/** + * Finds the first element in an array that validates a predicate. + * @param array + * @param function predicate + * @return number + */ +function findFirst(array, predicate) { + for (let i = 0, len = array.length; i < len; i++) { + let element = array[i]; + if (predicate(element)) return element; + } +} + +/** + * Finds the last element in an array that validates a predicate. + * @param array + * @param function predicate + * @return number + */ +function findLast(array, predicate) { + for (let i = array.length - 1; i >= 0; i--) { + let element = array[i]; + if (predicate(element)) return element; + } +} diff --git a/browser/themes/shared/devtools/widgets.inc.css b/browser/themes/shared/devtools/widgets.inc.css index ddcd34ec2ea..4b296583125 100644 --- a/browser/themes/shared/devtools/widgets.inc.css +++ b/browser/themes/shared/devtools/widgets.inc.css @@ -926,10 +926,6 @@ cursor: grabbing; } -.graph-widget-canvas ~ * { - pointer-events: none; -} - /* Line graph widget */ .line-graph-widget-canvas { @@ -944,6 +940,7 @@ top: 0; left: 0; border-right: 1px solid rgba(255,255,255,0.25); + pointer-events: none; } .line-graph-widget-gutter-line { @@ -976,6 +973,7 @@ transform: translateY(-50%); font-size: 80%; z-index: 1; + pointer-events: none; } .line-graph-widget-tooltip::before { @@ -1053,6 +1051,7 @@ left: 8px; color: #292e33; font-size: 80%; + pointer-events: none; } .bar-graph-widget-legend-item { @@ -1072,6 +1071,8 @@ border: 1px solid #fff; border-radius: 1px; -moz-margin-end: 4px; + pointer-events: all; + cursor: pointer; } .bar-graph-widget-legend-item > [view="label"] {