Bug 1034669 - Add a way of fading out certain areas of the canvas graphs, r=pbrosset

--HG--
rename : browser/devtools/shared/test/browser_graphs-11.js => browser/devtools/shared/test/browser_graphs-11a.js
This commit is contained in:
Victor Porof 2014-07-15 08:34:07 -04:00
parent 5e3dbbee87
commit daeecc00e7
6 changed files with 438 additions and 32 deletions

View File

@ -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]

View File

@ -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.");
}

View File

@ -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: () => {}
};
}

View File

@ -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;
}
}

View File

@ -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"] {